summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/benchmarks/banzai_benchmark.rb10
-rw-r--r--spec/components/layouts/horizontal_section_component_spec.rb88
-rw-r--r--spec/components/pajamas/badge_component_spec.rb148
-rw-r--r--spec/components/previews/layouts/horizontal_section_component_preview.rb22
-rw-r--r--spec/components/previews/pajamas/badge_component_preview.rb61
-rw-r--r--spec/config/metrics/aggregates/aggregated_metrics_spec.rb4
-rw-r--r--spec/config/object_store_settings_spec.rb69
-rw-r--r--spec/config/settings_spec.rb28
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb13
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb84
-rw-r--r--spec/controllers/admin/cohorts_controller_spec.rb12
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb4
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb33
-rw-r--r--spec/controllers/admin/topics_controller_spec.rb2
-rw-r--r--spec/controllers/admin/users_controller_spec.rb164
-rw-r--r--spec/controllers/application_controller_spec.rb2
-rw-r--r--spec/controllers/concerns/continue_params_spec.rb2
-rw-r--r--spec/controllers/concerns/product_analytics_tracking_spec.rb2
-rw-r--r--spec/controllers/concerns/redis_tracking_spec.rb3
-rw-r--r--spec/controllers/confirmations_controller_spec.rb4
-rw-r--r--spec/controllers/graphql_controller_spec.rb9
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb19
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb4
-rw-r--r--spec/controllers/groups/releases_controller_spec.rb4
-rw-r--r--spec/controllers/groups/settings/applications_controller_spec.rb84
-rw-r--r--spec/controllers/help_controller_spec.rb33
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb8
-rw-r--r--spec/controllers/import/github_controller_spec.rb10
-rw-r--r--spec/controllers/import/manifest_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb27
-rw-r--r--spec/controllers/oauth/token_info_controller_spec.rb12
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/personal_access_tokens_controller_spec.rb16
-rw-r--r--spec/controllers/profiles_controller_spec.rb8
-rw-r--r--spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb1
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb6
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb2
-rw-r--r--spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb74
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb9
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb12
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb4
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb126
-rw-r--r--spec/controllers/projects/feature_flags_controller_spec.rb6
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb4
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb15
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb136
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb8
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb73
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb91
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb3
-rw-r--r--spec/controllers/projects/pipelines/tests_controller_spec.rb11
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb87
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb32
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb2
-rw-r--r--spec/controllers/projects/service_desk_controller_spec.rb5
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb8
-rw-r--r--spec/controllers/projects/settings/merge_requests_controller_spec.rb52
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb6
-rw-r--r--spec/controllers/projects_controller_spec.rb36
-rw-r--r--spec/controllers/registrations/welcome_controller_spec.rb4
-rw-r--r--spec/controllers/registrations_controller_spec.rb33
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb4
-rw-r--r--spec/controllers/search_controller_spec.rb53
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb2
-rw-r--r--spec/db/development/add_security_training_providers_spec.rb9
-rw-r--r--spec/db/migration_spec.rb4
-rw-r--r--spec/db/production/add_security_training_providers_spec.rb9
-rw-r--r--spec/db/schema_spec.rb8
-rw-r--r--spec/dependencies/omniauth_saml_spec.rb2
-rw-r--r--spec/deprecation_toolkit_env.rb2
-rw-r--r--spec/experiments/application_experiment_spec.rb26
-rw-r--r--spec/factories/ci/build_trace_chunks.rb4
-rw-r--r--spec/factories/ci/builds.rb20
-rw-r--r--spec/factories/ci/job_artifacts.rb33
-rw-r--r--spec/factories/ci/pipeline_artifacts.rb2
-rw-r--r--spec/factories/ci/reports/sbom/components.rb19
-rw-r--r--spec/factories/ci/reports/sbom/sources.rb34
-rw-r--r--spec/factories/commit_statuses.rb13
-rw-r--r--spec/factories/design_management/designs.rb2
-rw-r--r--spec/factories/design_management/versions.rb4
-rw-r--r--spec/factories/emails.rb2
-rw-r--r--spec/factories/environments.rb2
-rw-r--r--spec/factories/external_pull_requests.rb2
-rw-r--r--spec/factories/generic_commit_statuses.rb8
-rw-r--r--spec/factories/gitlab/database/postgres_index.rb2
-rw-r--r--spec/factories/group_members.rb1
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/ml/candidates.rb7
-rw-r--r--spec/factories/ml/experiments.rb8
-rw-r--r--spec/factories/namespaces/sync_events.rb7
-rw-r--r--spec/factories/onboarding/progresses.rb (renamed from spec/factories/onboarding_progresses.rb)2
-rw-r--r--spec/factories/packages/debian/component_file.rb8
-rw-r--r--spec/factories/packages/dependencies.rb4
-rw-r--r--spec/factories/packages/package_files.rb8
-rw-r--r--spec/factories/packages/package_tags.rb2
-rw-r--r--spec/factories/packages/packages.rb20
-rw-r--r--spec/factories/packages/rpm/metadata.rb12
-rw-r--r--spec/factories/project_members.rb1
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/factories/prometheus_alert.rb2
-rw-r--r--spec/factories/prometheus_metrics.rb2
-rw-r--r--spec/factories/protected_branches.rb52
-rw-r--r--spec/factories/users/ghost_user_migrations.rb9
-rw-r--r--spec/factories/work_items.rb4
-rw-r--r--spec/fast_spec_helper.rb10
-rw-r--r--spec/features/admin/admin_mode/workers_spec.rb54
-rw-r--r--spec/features/admin/admin_mode_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb75
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb35
-rw-r--r--spec/features/admin/users/user_spec.rb4
-rw-r--r--spec/features/clusters/create_agent_spec.rb9
-rw-r--r--spec/features/commits/user_view_commits_spec.rb57
-rw-r--r--spec/features/cycle_analytics_spec.rb4
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/milestones_spec.rb7
-rw-r--r--spec/features/dashboard/todos/todos_sorting_spec.rb2
-rw-r--r--spec/features/discussion_comments/issue_spec.rb1
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb1
-rw-r--r--spec/features/groups/group_runners_spec.rb52
-rw-r--r--spec/features/groups/navbar_spec.rb20
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb40
-rw-r--r--spec/features/groups/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/features/groups_spec.rb71
-rw-r--r--spec/features/help_dropdown_spec.rb3
-rw-r--r--spec/features/ide/user_commits_changes_spec.rb2
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb2
-rw-r--r--spec/features/ide_spec.rb26
-rw-r--r--spec/features/incidents/incident_timeline_events_spec.rb58
-rw-r--r--spec/features/incidents/user_uses_quick_actions_spec.rb26
-rw-r--r--spec/features/invites_spec.rb2
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb11
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/issue_detail_spec.rb9
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb6
-rw-r--r--spec/features/issues/resource_label_events_spec.rb1
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb2
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb4
-rw-r--r--spec/features/issues/user_sees_empty_state_spec.rb4
-rw-r--r--spec/features/issues/user_sorts_issue_comments_spec.rb2
-rw-r--r--spec/features/jira_oauth_provider_authorize_spec.rb8
-rw-r--r--spec/features/markdown/markdown_spec.rb4
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb5
-rw-r--r--spec/features/merge_request/user_comments_on_merge_request_spec.rb1
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb183
-rw-r--r--spec/features/merge_request/user_edits_merge_request_spec.rb11
-rw-r--r--spec/features/merge_request/user_manages_subscription_spec.rb10
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb24
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb4
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb102
-rw-r--r--spec/features/populate_new_pipeline_vars_with_params_spec.rb42
-rw-r--r--spec/features/profile_spec.rb38
-rw-r--r--spec/features/profiles/active_sessions_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb21
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb57
-rw-r--r--spec/features/project_variables_spec.rb30
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/blobs/blame_spec.rb99
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb4
-rw-r--r--spec/features/projects/branches/user_creates_branch_spec.rb133
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/projects/commits/multi_view_diff_spec.rb5
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb6
-rw-r--r--spec/features/projects/environments/environment_spec.rb7
-rw-r--r--spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb5
-rw-r--r--spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb7
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb17
-rw-r--r--spec/features/projects/fork_spec.rb5
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb7
-rw-r--r--spec/features/projects/milestones/milestones_sorting_spec.rb2
-rw-r--r--spec/features/projects/navbar_spec.rb4
-rw-r--r--spec/features/projects/new_project_spec.rb14
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb4
-rw-r--r--spec/features/projects/pipelines/legacy_pipeline_spec.rb48
-rw-r--r--spec/features/projects/pipelines/legacy_pipelines_spec.rb13
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb30
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb73
-rw-r--r--spec/features/projects/releases/user_creates_release_spec.rb7
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb261
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb81
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb44
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb41
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb20
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb6
-rw-r--r--spec/features/projects/show/user_interacts_with_stars_spec.rb38
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb4
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb11
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb10
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb2
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb4
-rw-r--r--spec/features/projects_spec.rb100
-rw-r--r--spec/features/runners_spec.rb98
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb78
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb1
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb2
-rw-r--r--spec/features/tags/developer_creates_tag_spec.rb2
-rw-r--r--spec/features/user_opens_link_to_comment_spec.rb2
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb35
-rw-r--r--spec/features/users/login_spec.rb4
-rw-r--r--spec/features/users/show_spec.rb4
-rw-r--r--spec/features/users/signup_spec.rb7
-rw-r--r--spec/features/work_items/work_item_children_spec.rb110
-rw-r--r--spec/finders/bulk_imports/entities_finder_spec.rb18
-rw-r--r--spec/finders/ci/daily_build_group_report_results_finder_spec.rb22
-rw-r--r--spec/finders/ci/jobs_finder_spec.rb2
-rw-r--r--spec/finders/ci/pipelines_for_merge_request_finder_spec.rb20
-rw-r--r--spec/finders/ci/runners_finder_spec.rb14
-rw-r--r--spec/finders/concerns/packages/finder_helper_spec.rb6
-rw-r--r--spec/finders/container_repositories_finder_spec.rb8
-rw-r--r--spec/finders/context_commits_finder_spec.rb26
-rw-r--r--spec/finders/crm/organizations_finder_spec.rb71
-rw-r--r--spec/finders/database/batched_background_migrations_finder_spec.rb27
-rw-r--r--spec/finders/deploy_tokens/tokens_finder_spec.rb48
-rw-r--r--spec/finders/deployments_finder_spec.rb42
-rw-r--r--spec/finders/design_management/versions_finder_spec.rb4
-rw-r--r--spec/finders/environments/environments_finder_spec.rb10
-rw-r--r--spec/finders/group_descendants_finder_spec.rb6
-rw-r--r--spec/finders/group_members_finder_spec.rb55
-rw-r--r--spec/finders/groups/accepting_group_transfers_finder_spec.rb135
-rw-r--r--spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb14
-rw-r--r--spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb6
-rw-r--r--spec/finders/groups_finder_spec.rb102
-rw-r--r--spec/finders/merge_requests_finder/params_spec.rb23
-rw-r--r--spec/finders/merge_requests_finder_spec.rb58
-rw-r--r--spec/finders/milestones_finder_spec.rb2
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb4
-rw-r--r--spec/finders/packages/npm/package_finder_spec.rb2
-rw-r--r--spec/finders/projects_finder_spec.rb10
-rw-r--r--spec/finders/template_finder_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_noteable.json8
-rw-r--r--spec/fixtures/api/schemas/external_validation.json12
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json6
-rw-r--r--spec/fixtures/api/schemas/ml/get_experiment.json23
-rw-r--r--spec/fixtures/api/schemas/ml/run.json47
-rw-r--r--spec/fixtures/api/schemas/ml/update_run.json35
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/job.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/package.json7
-rw-r--r--spec/fixtures/blockquote_fence_after.md24
-rw-r--r--spec/fixtures/blockquote_fence_before.md24
-rw-r--r--spec/fixtures/cdn/google_cloud.json17
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb4
-rw-r--r--spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zipbin15902 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zipbin203718 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zipbin332 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zipbin177 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zipbin520 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zipbin1042247 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json74
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/project.json33
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson2
-rw-r--r--spec/fixtures/markdown.md.erb36
-rw-r--r--spec/fixtures/markdown/markdown_golden_master_examples.yml2
-rw-r--r--spec/fixtures/packages/debian/distribution/D-I-Packages2
-rw-r--r--spec/fixtures/packages/debian/distribution/OtherSHA2561
-rw-r--r--spec/fixtures/packages/debian/distribution/Sources2
-rw-r--r--spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpmbin0 -> 6604 bytes
-rw-r--r--spec/fixtures/security_reports/deprecated/gl-sast-report.json2
-rw-r--r--spec/fixtures/security_reports/feature-branch/gl-sast-report.json2
-rw-r--r--spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report-names.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json50
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report.json405
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-missing-scanner.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report-bandit.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report-gosec.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report-minimal.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report.json2
-rw-r--r--spec/fixtures/security_reports/master/gl-secret-detection-report.json2
-rw-r--r--spec/frontend/__helpers__/datetime_helpers.js2
-rw-r--r--spec/frontend/__helpers__/dl_locator_helper.js13
-rw-r--r--spec/frontend/__helpers__/keep_alive_component_helper_spec.js6
-rw-r--r--spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js4
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js3
-rw-r--r--spec/frontend/__mocks__/sortablejs/index.js2
-rw-r--r--spec/frontend/access_tokens/components/access_token_table_app_spec.js15
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js16
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js41
-rw-r--r--spec/frontend/access_tokens/index_spec.js2
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js12
-rw-r--r--spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js4
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js4
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js91
-rw-r--r--spec/frontend/alert_management/components/alert_management_empty_state_spec.js2
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js8
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js6
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js12
-rw-r--r--spec/frontend/alerts_settings/components/alerts_form_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js10
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js6
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js18
-rw-r--r--spec/frontend/analytics/components/activity_chart_spec.js2
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js15
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js6
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js6
-rw-r--r--spec/frontend/analytics/usage_trends/components/app_spec.js6
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js12
-rw-r--r--spec/frontend/analytics/usage_trends/components/users_chart_spec.js8
-rw-r--r--spec/frontend/analytics/usage_trends/utils_spec.js6
-rw-r--r--spec/frontend/api/harbor_registry_spec.js107
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js4
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js8
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js2
-rw-r--r--spec/frontend/autosave_spec.js44
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js6
-rw-r--r--spec/frontend/batch_comments/components/diff_file_drafts_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js8
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js2
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js2
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/review_bar_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js35
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js3
-rw-r--r--spec/frontend/behaviors/bind_in_out_spec.js6
-rw-r--r--spec/frontend/blob/sketch/index_spec.js22
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js20
-rw-r--r--spec/frontend/boards/board_list_helper.js1
-rw-r--r--spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap2
-rw-r--r--spec/frontend/boards/components/board_blocked_icon_spec.js74
-rw-r--r--spec/frontend/boards/components/board_card_move_to_position_spec.js133
-rw-r--r--spec/frontend/boards/components/board_card_spec.js9
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js2
-rw-r--r--spec/frontend/boards/components/issue_due_date_spec.js2
-rw-r--r--spec/frontend/boards/components/item_count_spec.js4
-rw-r--r--spec/frontend/boards/mock_data.js76
-rw-r--r--spec/frontend/boards/stores/actions_spec.js38
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js25
-rw-r--r--spec/frontend/branches/components/divergence_graph_spec.js4
-rw-r--r--spec/frontend/captcha/captcha_modal_spec.js2
-rw-r--r--spec/frontend/cascading_settings/components/lock_popovers_spec.js4
-rw-r--r--spec/frontend/chronic_duration_spec.js2
-rw-r--r--spec/frontend/ci_lint/components/ci_lint_spec.js6
-rw-r--r--spec/frontend/ci_secure_files/components/secure_files_list_spec.js4
-rw-r--r--spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js12
-rw-r--r--spec/frontend/ci_variable_list/components/ci_project_variables_spec.js215
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js30
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js8
-rw-r--r--spec/frontend/ci_variable_list/mocks.js15
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js2
-rw-r--r--spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js96
-rw-r--r--spec/frontend/clusters/agents/components/integration_status_spec.js111
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js6
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js6
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/clusters_main_view_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/node_error_help_text_spec.js2
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js4
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js8
-rw-r--r--spec/frontend/code_navigation/utils/index_spec.js4
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js216
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js4
-rw-r--r--spec/frontend/commit/mock_data.js211
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js27
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js6
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap62
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js126
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/code_block_spec.js)8
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/formatting_spec.js)11
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/link_spec.js)136
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/media_spec.js)13
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js25
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js213
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js26
-rw-r--r--spec/frontend/content_editor/components/loading_indicator_spec.js46
-rw-r--r--spec/frontend/content_editor/components/toolbar_image_button_spec.js21
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js18
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js17
-rw-r--r--spec/frontend/content_editor/components/toolbar_table_button_spec.js14
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js6
-rw-r--r--spec/frontend/content_editor/extensions/paste_markdown_spec.js21
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js73
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js6
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js95
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js21
-rw-r--r--spec/frontend/crm/form_spec.js5
-rw-r--r--spec/frontend/crm/mock_data.js22
-rw-r--r--spec/frontend/crm/organizations_root_spec.js92
-rw-r--r--spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap12
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/path_navigation_spec.js12
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js6
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js1
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js40
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js2
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js2
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap8
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap4
-rw-r--r--spec/frontend/design_management/pages/index_spec.js2
-rw-r--r--spec/frontend/diffs/components/app_spec.js36
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js6
-rw-r--r--spec/frontend/diffs/components/commit_widget_spec.js2
-rw-r--r--spec/frontend/diffs/components/compare_dropdown_layout_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js3
-rw-r--r--spec/frontend/diffs/components/diff_comment_cell_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js12
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js16
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js23
-rw-r--r--spec/frontend/diffs/components/diff_file_row_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js12
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js12
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_line_spec.js65
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js21
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js2
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js2
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js4
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_extension_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js6
-rw-r--r--spec/frontend/editor/source_editor_webide_ext_spec.js6
-rw-r--r--spec/frontend/emoji/components/category_spec.js10
-rw-r--r--spec/frontend/emoji/components/utils_spec.js4
-rw-r--r--spec/frontend/emoji/index_spec.js98
-rw-r--r--spec/frontend/environments/deployment_spec.js83
-rw-r--r--spec/frontend/environments/environment_table_spec.js2
-rw-r--r--spec/frontend/environments/environments_app_spec.js1
-rw-r--r--spec/frontend/environments/graphql/mock_data.js38
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js2
-rw-r--r--spec/frontend/environments/new_environment_spec.js2
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js10
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js2
-rw-r--r--spec/frontend/feature_flags/store/edit/actions_spec.js8
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js8
-rw-r--r--spec/frontend/feature_flags/store/new/actions_spec.js4
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js12
-rw-r--r--spec/frontend/filtered_search/droplab/drop_down_spec.js12
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb2
-rw-r--r--spec/frontend/fixtures/api_projects.rb2
-rw-r--r--spec/frontend/fixtures/application_settings.rb2
-rw-r--r--spec/frontend/fixtures/blob.rb2
-rw-r--r--spec/frontend/fixtures/branches.rb2
-rw-r--r--spec/frontend/fixtures/clusters.rb2
-rw-r--r--spec/frontend/fixtures/deploy_keys.rb8
-rw-r--r--spec/frontend/fixtures/groups.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/jobs.rb2
-rw-r--r--spec/frontend/fixtures/labels.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb2
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb2
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb2
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/raw.rb2
-rw-r--r--spec/frontend/fixtures/search.rb69
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/startup_css.rb16
-rw-r--r--spec/frontend/fixtures/todos.rb2
-rw-r--r--spec/frontend/flash_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js14
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js10
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js2
-rw-r--r--spec/frontend/frequent_items/utils_spec.js8
-rw-r--r--spec/frontend/google_cloud/databases/panel_spec.js17
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js45
-rw-r--r--spec/frontend/groups/components/app_spec.js14
-rw-r--r--spec/frontend/groups/components/empty_state_spec.js2
-rw-r--r--spec/frontend/groups/components/group_item_spec.js34
-rw-r--r--spec/frontend/groups/components/groups_spec.js4
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js14
-rw-r--r--spec/frontend/groups/components/item_caret_spec.js4
-rw-r--r--spec/frontend/groups/components/item_stats_spec.js2
-rw-r--r--spec/frontend/groups/components/item_stats_value_spec.js2
-rw-r--r--spec/frontend/groups/components/item_type_icon_spec.js2
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js187
-rw-r--r--spec/frontend/groups/components/visibility_level_dropdown_spec.js70
-rw-r--r--spec/frontend/header_search/init_spec.js10
-rw-r--r--spec/frontend/header_search/mock_data.js44
-rw-r--r--spec/frontend/header_search/store/actions_spec.js66
-rw-r--r--spec/frontend/header_search/store/getters_spec.js24
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js8
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js4
-rw-r--r--spec/frontend/ide/components/shared/tokened_input_spec.js2
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js62
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js2
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js18
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js4
-rw-r--r--spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js10
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js18
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js16
-rw-r--r--spec/frontend/import_entities/import_projects/store/getters_spec.js4
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js14
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js6
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js4
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js2
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js32
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js37
-rw-r--r--spec/frontend/issuable/components/issue_assignees_spec.js2
-rw-r--r--spec/frontend/issuable/components/issue_milestone_spec.js2
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js231
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js6
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_list_spec.js4
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js2
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js10
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js29
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js10
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_item_spec.js6
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js2
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js4
-rw-r--r--spec/frontend/issues/show/components/app_spec.js2
-rw-r--r--spec/frontend/issues/show/components/description_spec.js56
-rw-r--r--spec/frontend/issues/show/components/edit_actions_spec.js82
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js2
-rw-r--r--spec/frontend/issues/show/components/fields/title_spec.js2
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js10
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js17
-rw-r--r--spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js44
-rw-r--r--spec/frontend/issues/show/components/incidents/highlight_bar_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js8
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js42
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js40
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js27
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js157
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js19
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js6
-rw-r--r--spec/frontend/issues/show/components/pinned_links_spec.js2
-rw-r--r--spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js8
-rw-r--r--spec/frontend/jira_connect/branches/components/new_branch_form_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js118
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js21
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js87
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js33
-rw-r--r--spec/frontend/jira_connect/subscriptions/store/actions_spec.js16
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js10
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js15
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js2
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js2
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js34
-rw-r--r--spec/frontend/jobs/components/filtered_search/utils_spec.js19
-rw-r--r--spec/frontend/jobs/components/job/artifacts_block_spec.js (renamed from spec/frontend/jobs/components/artifacts_block_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/commit_block_spec.js (renamed from spec/frontend/jobs/components/commit_block_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/empty_state_spec.js (renamed from spec/frontend/jobs/components/empty_state_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/environments_block_spec.js (renamed from spec/frontend/jobs/components/environments_block_spec.js)4
-rw-r--r--spec/frontend/jobs/components/job/erased_block_spec.js (renamed from spec/frontend/jobs/components/erased_block_spec.js)4
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js (renamed from spec/frontend/jobs/components/job_app_spec.js)30
-rw-r--r--spec/frontend/jobs/components/job/job_container_item_spec.js (renamed from spec/frontend/jobs/components/job_container_item_spec.js)4
-rw-r--r--spec/frontend/jobs/components/job/job_log_controllers_spec.js (renamed from spec/frontend/jobs/components/job_log_controllers_spec.js)4
-rw-r--r--spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js (renamed from spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js)8
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js (renamed from spec/frontend/jobs/components/job_sidebar_details_container_spec.js)8
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js (renamed from spec/frontend/jobs/components/job_sidebar_retry_button_spec.js)4
-rw-r--r--spec/frontend/jobs/components/job/jobs_container_spec.js (renamed from spec/frontend/jobs/components/jobs_container_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js156
-rw-r--r--spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js91
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js (renamed from spec/frontend/jobs/components/manual_variables_form_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/sidebar_detail_row_spec.js (renamed from spec/frontend/jobs/components/sidebar_detail_row_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/sidebar_header_spec.js91
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js (renamed from spec/frontend/jobs/components/sidebar_spec.js)81
-rw-r--r--spec/frontend/jobs/components/job/stages_dropdown_spec.js (renamed from spec/frontend/jobs/components/stages_dropdown_spec.js)4
-rw-r--r--spec/frontend/jobs/components/job/stuck_block_spec.js (renamed from spec/frontend/jobs/components/stuck_block_spec.js)6
-rw-r--r--spec/frontend/jobs/components/job/trigger_block_spec.js (renamed from spec/frontend/jobs/components/trigger_block_spec.js)2
-rw-r--r--spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js (renamed from spec/frontend/jobs/components/unmet_prerequisites_block_spec.js)6
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_spec.js2
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js14
-rw-r--r--spec/frontend/jobs/store/actions_spec.js20
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js4
-rw-r--r--spec/frontend/labels/components/delete_label_modal_spec.js2
-rw-r--r--spec/frontend/lib/dompurify_spec.js2
-rw-r--r--spec/frontend/lib/gfm/index_spec.js376
-rw-r--r--spec/frontend/lib/utils/apollo_startup_js_link_spec.js2
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js2
-rw-r--r--spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js18
-rw-r--r--spec/frontend/lib/utils/finite_state_machine_spec.js4
-rw-r--r--spec/frontend/lib/utils/is_navigating_away_spec.js2
-rw-r--r--spec/frontend/lib/utils/navigation_utility_spec.js2
-rw-r--r--spec/frontend/lib/utils/poll_spec.js4
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js27
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js35
-rw-r--r--spec/frontend/lib/utils/vuex_module_mappers_spec.js2
-rw-r--r--spec/frontend/locale/sprintf_spec.js18
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js2
-rw-r--r--spec/frontend/members/mock_data.js1
-rw-r--r--spec/frontend/members/store/actions_spec.js4
-rw-r--r--spec/frontend/members/utils_spec.js16
-rw-r--r--spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js4
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js14
-rw-r--r--spec/frontend/merge_request_tabs_spec.js2
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js8
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap4
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js10
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js3
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js2
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js2
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js5
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js68
-rw-r--r--spec/frontend/nav/mock_data.js2
-rw-r--r--spec/frontend/notebook/cells/output/latex_spec.js2
-rw-r--r--spec/frontend/notebook/index_spec.js10
-rw-r--r--spec/frontend/notebook/lib/highlight_spec.js15
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js8
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js46
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js81
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js8
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js2
-rw-r--r--spec/frontend/notes/components/multiline_comment_form_spec.js2
-rw-r--r--spec/frontend/notes/components/note_actions/timeline_event_button_spec.js35
-rw-r--r--spec/frontend/notes/components/note_body_spec.js10
-rw-r--r--spec/frontend/notes/components/note_form_spec.js12
-rw-r--r--spec/frontend/notes/components/note_header_spec.js30
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js2
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js2
-rw-r--r--spec/frontend/notes/components/sort_discussion_spec.js102
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js31
-rw-r--r--spec/frontend/notes/stores/actions_spec.js101
-rw-r--r--spec/frontend/notes/stores/getters_spec.js6
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js2
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js14
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js11
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js143
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js75
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js85
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js38
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js52
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js75
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js66
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/mock_data.js269
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js162
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js42
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js125
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap18
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js143
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js108
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js54
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js54
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js115
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js164
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js109
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js64
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js96
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js (renamed from spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js)6
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js2
-rw-r--r--spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js2
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js4
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js8
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js6
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js10
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js115
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js173
-rw-r--r--spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js177
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js8
-rw-r--r--spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js59
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js223
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js2
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js50
-rw-r--r--spec/frontend/performance_bar/components/add_request_spec.js2
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js2
-rw-r--r--spec/frontend/persistent_user_callout_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/file-tree/container_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js109
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js3
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js2
-rw-r--r--spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js456
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js12
-rw-r--r--spec/frontend/pipeline_new/components/refs_dropdown_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js2
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js11
-rw-r--r--spec/frontend/pipeline_wizard/components/input_wrapper_spec.js2
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js125
-rw-r--r--spec/frontend/pipeline_wizard/mock/yaml.js2
-rw-r--r--spec/frontend/pipeline_wizard/pipeline_wizard_spec.js1
-rw-r--r--spec/frontend/pipelines/components/dag/dag_annotations_spec.js2
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js10
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js176
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js407
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js149
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js)5
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js)10
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js20
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js8
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js24
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js61
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/job_name_component_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js16
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js242
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js10
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js2
-rw-r--r--spec/frontend/pipelines/header_component_spec.js6
-rw-r--r--spec/frontend/pipelines/performance_insights_modal_spec.js131
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js20
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js71
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js24
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js5
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js18
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js72
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js8
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js2
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js4
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js7
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_source_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js7
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js7
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js7
-rw-r--r--spec/frontend/pipelines/utils_spec.js44
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js10
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js2
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js6
-rw-r--r--spec/frontend/profile/preferences/components/integration_view_spec.js2
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js4
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js8
-rw-r--r--spec/frontend/projects/commit/store/mutations_spec.js2
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js10
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js4
-rw-r--r--spec/frontend/projects/compare/components/repo_dropdown_spec.js4
-rw-r--r--spec/frontend/projects/compare/components/revision_card_spec.js4
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js4
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js6
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js2
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js2
-rw-r--r--spec/frontend/projects/details/upload_button_spec.js6
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js17
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js4
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js10
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js8
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js6
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js49
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js58
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js25
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js40
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js37
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js6
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js17
-rw-r--r--spec/frontend/related_issues/components/related_issuable_input_spec.js8
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js4
-rw-r--r--spec/frontend/releases/components/app_show_spec.js4
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js14
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js14
-rw-r--r--spec/frontend/releases/components/issuable_stats_spec.js8
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js12
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js8
-rw-r--r--spec/frontend/releases/components/release_block_spec.js6
-rw-r--r--spec/frontend/releases/components/release_skeleton_loader_spec.js2
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js2
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js6
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js26
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js19
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js7
-rw-r--r--spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js8
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js2
-rw-r--r--spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js2
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js2
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js8
-rw-r--r--spec/frontend/reports/components/report_item_spec.js4
-rw-r--r--spec/frontend/reports/grouped_test_report/components/modal_spec.js4
-rw-r--r--spec/frontend/reports/grouped_test_report/store/actions_spec.js4
-rw-r--r--spec/frontend/reports/mock_data/new_failures_with_null_files_report.json40
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap3
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js10
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js49
-rw-r--r--spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js2
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js20
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js2
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js9
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js2
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/index_spec.js39
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js2
-rw-r--r--spec/frontend/repository/log_tree_spec.js8
-rw-r--r--spec/frontend/repository/mock_data.js1
-rw-r--r--spec/frontend/repository/utils/commit_spec.js2
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js2
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js30
-rw-r--r--spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js164
-rw-r--r--spec/frontend/runner/components/cells/runner_status_cell_spec.js21
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js91
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_field_spec.js49
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js30
-rw-r--r--spec/frontend/runner/components/runner_header_spec.js6
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js75
-rw-r--r--spec/frontend/runner/components/runner_paused_badge_spec.js5
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js65
-rw-r--r--spec/frontend/runner/components/runner_stacked_layout_banner_spec.js39
-rw-r--r--spec/frontend/runner/components/runner_status_badge_spec.js20
-rw-r--r--spec/frontend/runner/components/runner_tag_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_tags_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_type_badge_spec.js17
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js7
-rw-r--r--spec/frontend/runner/components/stat/runner_stats_spec.js33
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js55
-rw-r--r--spec/frontend/runner/runner_edit/runner_edit_app_spec.js (renamed from spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js)13
-rw-r--r--spec/frontend/runner/utils_spec.js13
-rw-r--r--spec/frontend/search/sidebar/components/radio_filter_spec.js2
-rw-r--r--spec/frontend/search/sort/components/app_spec.js2
-rw-r--r--spec/frontend/set_status_modal/set_status_form_spec.js167
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js45
-rw-r--r--spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js156
-rw-r--r--spec/frontend/set_status_modal/utils_spec.js3
-rw-r--r--spec/frontend/sidebar/assignee_title_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js12
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js8
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js2
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js25
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js5
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js12
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js2
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js18
-rw-r--r--spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js2
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js2
-rw-r--r--spec/frontend/sidebar/issuable_assignees_spec.js23
-rw-r--r--spec/frontend/sidebar/lock/issuable_lock_form_spec.js2
-rw-r--r--spec/frontend/sidebar/mock_data.js10
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js2
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js11
-rw-r--r--spec/frontend/sidebar/todo_spec.js2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js38
-rw-r--r--spec/frontend/snippets/components/show_spec.js22
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js20
-rw-r--r--spec/frontend/surveys/merge_request_performance/app_spec.js74
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js2
-rw-r--r--spec/frontend/token_access/mock_data.js13
-rw-r--r--spec/frontend/token_access/token_access_spec.js28
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js4
-rw-r--r--spec/frontend/user_lists/store/index/actions_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js28
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js75
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js40
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap468
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap24
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js21
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js22
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js65
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js15
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js20
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js17
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/app_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js174
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js10
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js2
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap26
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js82
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/user_callout_dismisser_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js41
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js6
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js4
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js48
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js79
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js (renamed from spec/frontend/work_items/pages/work_item_detail_spec.js)120
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js346
-rw-r--r--spec/frontend/work_items/components/work_item_information_spec.js9
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js122
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js98
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js39
-rw-r--r--spec/frontend/work_items/components/work_item_weight_spec.js214
-rw-r--r--spec/frontend/work_items/mock_data.js258
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js4
-rw-r--r--spec/frontend/work_items/router_spec.js39
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js2
-rw-r--r--spec/frontend_integration/content_editor/content_editor_integration_spec.js111
-rw-r--r--spec/frontend_integration/ide/helpers/start.js4
-rw-r--r--spec/graphql/features/feature_flag_spec.rb52
-rw-r--r--spec/graphql/mutations/boards/issues/issue_move_list_spec.rb46
-rw-r--r--spec/graphql/mutations/ci/runner/update_spec.rb170
-rw-r--r--spec/graphql/mutations/commits/create_spec.rb289
-rw-r--r--spec/graphql/mutations/environments/canary_ingress/update_spec.rb11
-rw-r--r--spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb4
-rw-r--r--spec/graphql/mutations/merge_requests/create_spec.rb119
-rw-r--r--spec/graphql/mutations/releases/create_spec.rb2
-rw-r--r--spec/graphql/mutations/releases/update_spec.rb4
-rw-r--r--spec/graphql/resolvers/board_lists_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/config_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/group_runners_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/ci/jobs_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb69
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/test_suite_resolver_spec.rb11
-rw-r--r--spec/graphql/resolvers/container_repositories_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/container_repository_tags_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/crm/organization_state_counts_resolver_spec.rb61
-rw-r--r--spec/graphql/resolvers/crm/organizations_resolver_spec.rb14
-rw-r--r--spec/graphql/resolvers/deployment_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/deployments_resolver_spec.rb41
-rw-r--r--spec/graphql/resolvers/design_management/versions_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/environments/last_deployment_resolver_spec.rb40
-rw-r--r--spec/graphql/resolvers/group_members_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb52
-rw-r--r--spec/graphql/resolvers/project_members_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/work_items_resolver_spec.rb56
-rw-r--r--spec/graphql/types/base_field_spec.rb99
-rw-r--r--spec/graphql/types/branch_protections/merge_access_level_type_spec.rb13
-rw-r--r--spec/graphql/types/branch_protections/push_access_level_type_spec.rb13
-rw-r--r--spec/graphql/types/branch_rule_type_spec.rb22
-rw-r--r--spec/graphql/types/branch_rules/branch_protection_type_spec.rb13
-rw-r--r--spec/graphql/types/ci/config_variable_type_spec.rb7
-rw-r--r--spec/graphql/types/ci/group_variable_connection_type_spec.rb11
-rw-r--r--spec/graphql/types/ci/instance_variable_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/job_artifact_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/job_token_scope_type_spec.rb4
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/manual_variable_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/project_variable_connection_type_spec.rb11
-rw-r--r--spec/graphql/types/ci/runner_architecture_type_spec.rb4
-rw-r--r--spec/graphql/types/ci/runner_platform_type_spec.rb6
-rw-r--r--spec/graphql/types/ci/variable_interface_spec.rb2
-rw-r--r--spec/graphql/types/clusters/agent_type_spec.rb2
-rw-r--r--spec/graphql/types/customer_relations/organization_sort_enum_spec.rb22
-rw-r--r--spec/graphql/types/customer_relations/organization_state_counts_type_spec.rb31
-rw-r--r--spec/graphql/types/deployment_details_type_spec.rb17
-rw-r--r--spec/graphql/types/deployment_type_spec.rb17
-rw-r--r--spec/graphql/types/detployment_tag_type_spec.rb15
-rw-r--r--spec/graphql/types/environment_type_spec.rb10
-rw-r--r--spec/graphql/types/group_type_spec.rb12
-rw-r--r--spec/graphql/types/merge_request_review_state_enum_spec.rb4
-rw-r--r--spec/graphql/types/metrics/dashboard_type_spec.rb4
-rw-r--r--spec/graphql/types/packages/composer/metadatum_type_spec.rb2
-rw-r--r--spec/graphql/types/packages/package_type_enum_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb67
-rw-r--r--spec/graphql/types/subscription_type_spec.rb3
-rw-r--r--spec/graphql/types/timelog_type_spec.rb2
-rw-r--r--spec/graphql/types/user_merge_request_interaction_type_spec.rb3
-rw-r--r--spec/graphql/types/work_item_type_spec.rb4
-rw-r--r--spec/graphql/types/work_items/widgets/description_type_spec.rb2
-rw-r--r--spec/helpers/application_settings_helper_spec.rb70
-rw-r--r--spec/helpers/blob_helper_spec.rb2
-rw-r--r--spec/helpers/ci/builds_helper_spec.rb4
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb37
-rw-r--r--spec/helpers/commits_helper_spec.rb2
-rw-r--r--spec/helpers/gitlab_script_tag_helper_spec.rb4
-rw-r--r--spec/helpers/groups_helper_spec.rb23
-rw-r--r--spec/helpers/issuables_helper_spec.rb4
-rw-r--r--spec/helpers/jira_connect_helper_spec.rb32
-rw-r--r--spec/helpers/learn_gitlab_helper_spec.rb14
-rw-r--r--spec/helpers/members_helper_spec.rb8
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb2
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb240
-rw-r--r--spec/helpers/notify_helper_spec.rb27
-rw-r--r--spec/helpers/page_layout_helper_spec.rb16
-rw-r--r--spec/helpers/profiles_helper_spec.rb2
-rw-r--r--spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb25
-rw-r--r--spec/helpers/projects/pages_helper_spec.rb68
-rw-r--r--spec/helpers/projects/pipeline_helper_spec.rb8
-rw-r--r--spec/helpers/projects_helper_spec.rb36
-rw-r--r--spec/helpers/routing/pseudonymization_helper_spec.rb4
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/helpers/sorting_helper_spec.rb48
-rw-r--r--spec/helpers/storage_helper_spec.rb159
-rw-r--r--spec/helpers/tab_helper_spec.rb2
-rw-r--r--spec/helpers/todos_helper_spec.rb15
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb4
-rw-r--r--spec/helpers/users_helper_spec.rb32
-rw-r--r--spec/helpers/wiki_helper_spec.rb8
-rw-r--r--spec/helpers/wiki_page_version_helper_spec.rb4
-rw-r--r--spec/initializers/00_rails_disable_joins_spec.rb6
-rw-r--r--spec/initializers/action_cable_subscription_adapter_identifier_spec.rb2
-rw-r--r--spec/initializers/carrierwave_patch_spec.rb6
-rw-r--r--spec/initializers/load_balancing_spec.rb100
-rw-r--r--spec/initializers/microsoft_graph_mailer_spec.rb56
-rw-r--r--spec/initializers/net_http_patch_spec.rb1
-rw-r--r--spec/initializers/settings_spec.rb36
-rw-r--r--spec/initializers/trusted_proxies_spec.rb2
-rw-r--r--spec/lib/api/entities/ci/job_request/image_spec.rb10
-rw-r--r--spec/lib/api/entities/ci/job_request/service_spec.rb8
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_info_spec.rb65
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_spec.rb21
-rw-r--r--spec/lib/api/entities/personal_access_token_with_details_spec.rb29
-rw-r--r--spec/lib/api/helpers/caching_spec.rb9
-rw-r--r--spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb50
-rw-r--r--spec/lib/api/helpers/packages_helpers_spec.rb44
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb2
-rw-r--r--spec/lib/api/helpers_spec.rb89
-rw-r--r--spec/lib/api/integrations/slack/events/url_verification_spec.rb11
-rw-r--r--spec/lib/backup/database_backup_error_spec.rb2
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb4
-rw-r--r--spec/lib/backup/task_spec.rb2
-rw-r--r--spec/lib/banzai/color_parser_spec.rb2
-rw-r--r--spec/lib/banzai/filter/blockquote_fence_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/kroki_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb298
-rw-r--r--spec/lib/banzai/filter/output_safety_spec.rb2
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/repository_link_filter_spec.rb14
-rw-r--r--spec/lib/banzai/filter_array_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb17
-rw-r--r--spec/lib/banzai/pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/querying_spec.rb2
-rw-r--r--spec/lib/banzai/renderer_spec.rb2
-rw-r--r--spec/lib/bitbucket/collection_spec.rb2
-rw-r--r--spec/lib/bitbucket/page_spec.rb2
-rw-r--r--spec/lib/bitbucket/paginator_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/comment_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/issue_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/pull_request_comment_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/pull_request_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/repo_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/user_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/page_spec.rb2
-rw-r--r--spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb16
-rw-r--r--spec/lib/bulk_imports/file_downloads/validations_spec.rb28
-rw-r--r--spec/lib/bulk_imports/pipeline/extracted_data_spec.rb2
-rw-r--r--spec/lib/bulk_imports/pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb51
-rw-r--r--spec/lib/bulk_imports/retry_pipeline_error_spec.rb2
-rw-r--r--spec/lib/constraints/jira_encoded_url_constrainer_spec.rb2
-rw-r--r--spec/lib/container_registry/gitlab_api_client_spec.rb4
-rw-r--r--spec/lib/container_registry/tag_spec.rb25
-rw-r--r--spec/lib/declarative_enum_spec.rb2
-rw-r--r--spec/lib/error_tracking/sentry_client/event_spec.rb5
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_link_spec.rb7
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_spec.rb28
-rw-r--r--spec/lib/error_tracking/sentry_client/projects_spec.rb5
-rw-r--r--spec/lib/error_tracking/sentry_client/repo_spec.rb3
-rw-r--r--spec/lib/error_tracking/sentry_client_spec.rb2
-rw-r--r--spec/lib/gitlab/alert_management/payload/base_spec.rb22
-rw-r--r--spec/lib/gitlab/alert_management/payload/generic_spec.rb36
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb2
-rw-r--r--spec/lib/gitlab/analytics/date_filler_spec.rb136
-rw-r--r--spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc/html5_converter_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb6
-rw-r--r--spec/lib/gitlab/audit/auditor_spec.rb9
-rw-r--r--spec/lib/gitlab/audit/null_target_spec.rb2
-rw-r--r--spec/lib/gitlab/audit/type/definition_spec.rb219
-rw-r--r--spec/lib/gitlab/auth/ldap/auth_hash_spec.rb14
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb202
-rw-r--r--spec/lib/gitlab/auth/ldap/person_spec.rb12
-rw-r--r--spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/o_auth/provider_spec.rb18
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb107
-rw-r--r--spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb30
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb57
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb42
-rw-r--r--spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb32
-rw-r--r--spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb24
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb135
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb133
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb112
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb23
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb7
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb89
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb102
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb114
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb59
-rw-r--r--spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb92
-rw-r--r--spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb31
-rw-r--r--spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb91
-rw-r--r--spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb21
-rw-r--r--spec/lib/gitlab/bullet/exclusions_spec.rb1
-rw-r--r--spec/lib/gitlab/cache/helpers_spec.rb9
-rw-r--r--spec/lib/gitlab/changes_list_spec.rb2
-rw-r--r--spec/lib/gitlab/chat/responder/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2json/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2json/result_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/path_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/build/policy_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/port_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb173
-rw-r--r--spec/lib/gitlab/ci/config/entry/port_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb149
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/config/entry/variable_spec.rb212
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb82
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/mask_secret_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb273
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/parsers_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/pipeline/duration_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb68
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb82
-rw-r--r--spec/lib/gitlab/ci/project_config/repository_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/project_config/source_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/project_config_spec.rb177
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/coverage_report_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/component_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/report_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/source_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/reports/security/flag_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/security/link_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/security/scan_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/terraform_reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/extended_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/group/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/trace/archive_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/variables/collection/sort_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/variables/helpers_spec.rb113
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/dag_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/result_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb185
-rw-r--r--spec/lib/gitlab/ci_access_spec.rb2
-rw-r--r--spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb168
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb22
-rw-r--r--spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/attributable_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/composable_hash_spec.rb9
-rw-r--r--spec/lib/gitlab/config/entry/simplifiable_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/undefined_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/unspecified_spec.rb2
-rw-r--r--spec/lib/gitlab/container_repository/tags/cache_spec.rb12
-rw-r--r--spec/lib/gitlab/cross_project_access/check_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/cross_project_access/check_info_spec.rb2
-rw-r--r--spec/lib/gitlab/cross_project_access/class_methods_spec.rb2
-rw-r--r--spec/lib/gitlab/cross_project_access_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/summary/value_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/issuable_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb32
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb68
-rw-r--r--spec/lib/gitlab/database/batch_average_counter_spec.rb107
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb177
-rw-r--r--spec/lib/gitlab/database/lock_writes_manager_spec.rb84
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb27
-rw-r--r--spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb246
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb89
-rw-r--r--spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb15
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb6
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb27
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb70
-rw-r--r--spec/lib/gitlab/database/partitioning_spec.rb73
-rw-r--r--spec/lib/gitlab/database/postgres_constraint_spec.rb123
-rw-r--r--spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb78
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb22
-rw-r--r--spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb41
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb257
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb4
-rw-r--r--spec/lib/gitlab/database_spec.rb20
-rw-r--r--spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_collection_sorter_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb110
-rw-r--r--spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/lines_unfolder_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb3
-rw-r--r--spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb45
-rw-r--r--spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb (renamed from spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb)2
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb30
-rw-r--r--spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb8
-rw-r--r--spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb2
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb10
-rw-r--r--spec/lib/gitlab/etag_caching/router/graphql_spec.rb2
-rw-r--r--spec/lib/gitlab/experimentation/group_types_spec.rb2
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb2
-rw-r--r--spec/lib/gitlab/file_markdown_link_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb209
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb2
-rw-r--r--spec/lib/gitlab/git/changed_path_spec.rb2
-rw-r--r--spec/lib/gitlab/git/changes_spec.rb2
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb70
-rw-r--r--spec/lib/gitlab/git/commit_stats_spec.rb16
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb5
-rw-r--r--spec/lib/gitlab/git/conflict/file_spec.rb2
-rw-r--r--spec/lib/gitlab/git/conflict/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git/cross_repo_comparer_spec.rb2
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb18
-rw-r--r--spec/lib/gitlab/git/gitmodules_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git/lfs_pointer_file_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb613
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb2
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb2
-rw-r--r--spec/lib/gitlab/git/util_spec.rb2
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb63
-rw-r--r--spec/lib/gitlab/git_spec.rb12
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb19
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb79
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb107
-rw-r--r--spec/lib/gitlab/gitaly_client/server_service_spec.rb42
-rw-r--r--spec/lib/gitlab/gitaly_client/util_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/attachments_downloader_spec.rb97
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb62
-rw-r--r--spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb55
-rw-r--r--spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb48
-rw-r--r--spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb49
-rw-r--r--spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb101
-rw-r--r--spec/lib/gitlab/github_import/importer/events/closed_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb95
-rw-r--r--spec/lib/gitlab/github_import/importer/events/renamed_spec.rb50
-rw-r--r--spec/lib/gitlab/github_import/importer/events/reopened_spec.rb63
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb91
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb225
-rw-r--r--spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb74
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb74
-rw-r--r--spec/lib/gitlab/github_import/markdown_text_spec.rb28
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb32
-rw-r--r--spec/lib/gitlab/github_import/representation/issue_event_spec.rb56
-rw-r--r--spec/lib/gitlab/github_import/representation/lfs_object_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/note_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/protected_branch_spec.rb51
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/release_attachments_spec.rb49
-rw-r--r--spec/lib/gitlab/github_import/representation/to_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/user_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb12
-rw-r--r--spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/limit/field_call_count_spec.rb66
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb403
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb27
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb24
-rw-r--r--spec/lib/gitlab/health_checks/master_check_spec.rb2
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb20
-rw-r--r--spec/lib/gitlab/health_checks/redis/cache_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis/queues_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis/redis_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/redis_spec.rb26
-rw-r--r--spec/lib/gitlab/i18n/metadata_entry_spec.rb2
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb2
-rw-r--r--spec/lib/gitlab/import/merge_request_creator_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml30
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attributes_finder_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/file_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/object_builder_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb65
-rw-r--r--spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml29
-rw-r--r--spec/lib/gitlab/import_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb20
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb2
-rw-r--r--spec/lib/gitlab/insecure_key_fingerprint_spec.rb2
-rw-r--r--spec/lib/gitlab/instrumentation/redis_base_spec.rb19
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb40
-rw-r--r--spec/lib/gitlab/instrumentation/redis_spec.rb12
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb14
-rw-r--r--spec/lib/gitlab/internal_post_receive/response_spec.rb2
-rw-r--r--spec/lib/gitlab/jira/middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/jira_import/metadata_collector_spec.rb2
-rw-r--r--spec/lib/gitlab/jira_import_spec.rb6
-rw-r--r--spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb2
-rw-r--r--spec/lib/gitlab/lazy_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/client_spec.rb24
-rw-r--r--spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb24
-rw-r--r--spec/lib/gitlab/loop_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb47
-rw-r--r--spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb4
-rw-r--r--spec/lib/gitlab/markdown_cache/redis/extension_spec.rb8
-rw-r--r--spec/lib/gitlab/markup_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/jemalloc_spec.rb12
-rw-r--r--spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb23
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb318
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/dashboard/defaults_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb38
-rw-r--r--spec/lib/gitlab/metrics/dashboard/validator_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/delta_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb22
-rw-r--r--spec/lib/gitlab/metrics/global_search_slis_spec.rb173
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb10
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb37
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb1
-rw-r--r--spec/lib/gitlab/middleware/release_env_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb2
-rw-r--r--spec/lib/gitlab/namespaced_session_store_spec.rb2
-rw-r--r--spec/lib/gitlab/nav/top_nav_menu_header_spec.rb16
-rw-r--r--spec/lib/gitlab/nav/top_nav_menu_item_spec.rb4
-rw-r--r--spec/lib/gitlab/net_http_adapter_spec.rb2
-rw-r--r--spec/lib/gitlab/null_request_store_spec.rb2
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb12
-rw-r--r--spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb29
-rw-r--r--spec/lib/gitlab/phabricator_import/representation/task_spec.rb2
-rw-r--r--spec/lib/gitlab/phabricator_import/representation/user_spec.rb2
-rw-r--r--spec/lib/gitlab/popen/runner_spec.rb2
-rw-r--r--spec/lib/gitlab/push_options_spec.rb2
-rw-r--r--spec/lib/gitlab/quick_actions/substitution_definition_spec.rb2
-rw-r--r--spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb94
-rw-r--r--spec/lib/gitlab/redis/boolean_spec.rb2
-rw-r--r--spec/lib/gitlab/redis/cache_spec.rb4
-rw-r--r--spec/lib/gitlab/redis/duplicate_jobs_spec.rb2
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb44
-rw-r--r--spec/lib/gitlab/redis/sidekiq_status_spec.rb2
-rw-r--r--spec/lib/gitlab/render_timeout_spec.rb2
-rw-r--r--spec/lib/gitlab/seeder_spec.rb40
-rw-r--r--spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb43
-rw-r--r--spec/lib/gitlab/service_desk_email_spec.rb2
-rw-r--r--spec/lib/gitlab/session_spec.rb2
-rw-r--r--spec/lib/gitlab/setup_helper/workhorse_spec.rb2
-rw-r--r--spec/lib/gitlab/shell_spec.rb53
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_death_handler_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb48
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_queue_spec.rb10
-rw-r--r--spec/lib/gitlab/sidekiq_signals_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_versioning_spec.rb25
-rw-r--r--spec/lib/gitlab/slug/environment_spec.rb28
-rw-r--r--spec/lib/gitlab/spamcheck/client_spec.rb59
-rw-r--r--spec/lib/gitlab/string_placeholder_replacer_spec.rb2
-rw-r--r--spec/lib/gitlab/string_range_marker_spec.rb2
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb2
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb15
-rw-r--r--spec/lib/gitlab/tcp_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/tracking/incident_management_spec.rb2
-rw-r--r--spec/lib/gitlab/tracking_spec.rb44
-rw-r--r--spec/lib/gitlab/tree_summary_spec.rb16
-rw-r--r--spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb2
-rw-r--r--spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb66
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb35
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb22
-rw-r--r--spec/lib/gitlab/usage_data/topology_spec.rb16
-rw-r--r--spec/lib/gitlab/usage_data_counters/base_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb68
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/note_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb29
-rw-r--r--spec/lib/gitlab/utils/deep_size_spec.rb8
-rw-r--r--spec/lib/gitlab/utils/delegator_override_spec.rb1
-rw-r--r--spec/lib/gitlab/utils/execution_tracker_spec.rb24
-rw-r--r--spec/lib/gitlab/utils/json_size_estimator_spec.rb2
-rw-r--r--spec/lib/gitlab/utils/markdown_spec.rb2
-rw-r--r--spec/lib/gitlab/utils/merge_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/utils/nokogiri_spec.rb3
-rw-r--r--spec/lib/gitlab/utils/sanitize_node_link_spec.rb4
-rw-r--r--spec/lib/gitlab/utils_spec.rb2
-rw-r--r--spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb23
-rw-r--r--spec/lib/gitlab/word_diff/chunk_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/word_diff/line_processor_spec.rb2
-rw-r--r--spec/lib/gitlab/word_diff/parser_spec.rb20
-rw-r--r--spec/lib/gitlab/word_diff/positions_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/word_diff/segments/chunk_spec.rb2
-rw-r--r--spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb2
-rw-r--r--spec/lib/gitlab/word_diff/segments/newline_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb2
-rw-r--r--spec/lib/gitlab_edition_spec.rb55
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb2
-rw-r--r--spec/lib/marginalia_spec.rb20
-rw-r--r--spec/lib/microsoft_teams/activity_spec.rb2
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb98
-rw-r--r--spec/lib/omni_auth/strategies/bitbucket_spec.rb29
-rw-r--r--spec/lib/peek/views/redis_detailed_spec.rb22
-rw-r--r--spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb1
-rw-r--r--spec/lib/security/ci_configuration/sast_build_action_spec.rb58
-rw-r--r--spec/lib/security/report_schema_version_matcher_spec.rb2
-rw-r--r--spec/lib/security/weak_passwords_spec.rb112
-rw-r--r--spec/lib/sidebars/concerns/container_with_html_options_spec.rb2
-rw-r--r--spec/lib/sidebars/concerns/link_with_html_options_spec.rb2
-rw-r--r--spec/lib/sidebars/groups/menus/observability_menu_spec.rb43
-rw-r--r--spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/groups/menus/settings_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/menu_item_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/deployments_menu_spec.rb36
-rw-r--r--spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb8
-rw-r--r--spec/lib/sidebars/projects/menus/monitor_menu_spec.rb25
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb18
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb8
-rw-r--r--spec/lib/system_check/base_check_spec.rb2
-rw-r--r--spec/mailers/emails/pipelines_spec.rb4
-rw-r--r--spec/mailers/emails/service_desk_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb11
-rw-r--r--spec/mailers/previews_spec.rb44
-rw-r--r--spec/mailers/repository_check_mailer_spec.rb9
-rw-r--r--spec/migrations/20210804150320_create_base_work_item_types_spec.rb6
-rw-r--r--spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb2
-rw-r--r--spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb6
-rw-r--r--spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb2
-rw-r--r--spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb12
-rw-r--r--spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb8
-rw-r--r--spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb43
-rw-r--r--spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb42
-rw-r--r--spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb62
-rw-r--r--spec/migrations/20220809002011_schedule_destroy_invalid_group_members_spec.rb31
-rw-r--r--spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb73
-rw-r--r--spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb23
-rw-r--r--spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb23
-rw-r--r--spec/migrations/20220901035725_schedule_destroy_invalid_project_members_spec.rb31
-rw-r--r--spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb62
-rw-r--r--spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb26
-rw-r--r--spec/migrations/backfill_namespace_id_on_issues_spec.rb32
-rw-r--r--spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb32
-rw-r--r--spec/migrations/confirm_support_bot_user_spec.rb2
-rw-r--r--spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb108
-rw-r--r--spec/migrations/orphaned_invited_members_cleanup_spec.rb46
-rw-r--r--spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb54
-rw-r--r--spec/migrations/reset_job_token_scope_enabled_again_spec.rb4
-rw-r--r--spec/migrations/reset_job_token_scope_enabled_spec.rb4
-rw-r--r--spec/migrations/reset_severity_levels_to_new_default_spec.rb10
-rw-r--r--spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb24
-rw-r--r--spec/models/application_setting_spec.rb8
-rw-r--r--spec/models/ci/build_dependencies_spec.rb23
-rw-r--r--spec/models/ci/build_spec.rb441
-rw-r--r--spec/models/ci/freeze_period_status_spec.rb9
-rw-r--r--spec/models/ci/job_artifact_spec.rb132
-rw-r--r--spec/models/ci/namespace_mirror_spec.rb38
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb13
-rw-r--r--spec/models/ci/pipeline_spec.rb352
-rw-r--r--spec/models/ci/pipeline_variable_spec.rb21
-rw-r--r--spec/models/ci/processable_spec.rb20
-rw-r--r--spec/models/ci/runner_spec.rb188
-rw-r--r--spec/models/ci/stage_spec.rb29
-rw-r--r--spec/models/ci/trigger_spec.rb17
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb16
-rw-r--r--spec/models/commit_signatures/gpg_signature_spec.rb17
-rw-r--r--spec/models/commit_signatures/ssh_signature_spec.rb12
-rw-r--r--spec/models/commit_signatures/x509_commit_signature_spec.rb11
-rw-r--r--spec/models/commit_spec.rb16
-rw-r--r--spec/models/commit_status_spec.rb59
-rw-r--r--spec/models/concerns/approvable_spec.rb (renamed from spec/models/concerns/approvable_base_spec.rb)2
-rw-r--r--spec/models/concerns/ci/artifactable_spec.rb24
-rw-r--r--spec/models/concerns/ci/has_deployment_name_spec.rb34
-rw-r--r--spec/models/concerns/ci/track_environment_usage_spec.rb61
-rw-r--r--spec/models/concerns/counter_attribute_spec.rb10
-rw-r--r--spec/models/concerns/from_set_operator_spec.rb51
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb1
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb1
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb6
-rw-r--r--spec/models/container_registry/event_spec.rb2
-rw-r--r--spec/models/customer_relations/organization_spec.rb36
-rw-r--r--spec/models/deployment_spec.rb101
-rw-r--r--spec/models/design_management/version_spec.rb2
-rw-r--r--spec/models/environment_spec.rb79
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb106
-rw-r--r--spec/models/group_group_link_spec.rb11
-rw-r--r--spec/models/group_spec.rb343
-rw-r--r--spec/models/incident_management/timeline_event_spec.rb16
-rw-r--r--spec/models/integrations/chat_message/pipeline_message_spec.rb4
-rw-r--r--spec/models/integrations/datadog_spec.rb9
-rw-r--r--spec/models/integrations/discord_spec.rb4
-rw-r--r--spec/models/integrations/drone_ci_spec.rb4
-rw-r--r--spec/models/integrations/hangouts_chat_spec.rb171
-rw-r--r--spec/models/integrations/harbor_spec.rb6
-rw-r--r--spec/models/issue_spec.rb32
-rw-r--r--spec/models/jira_connect_installation_spec.rb26
-rw-r--r--spec/models/loose_foreign_keys/deleted_record_spec.rb4
-rw-r--r--spec/models/member_spec.rb12
-rw-r--r--spec/models/members/group_member_spec.rb7
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/merge_request_assignee_spec.rb22
-rw-r--r--spec/models/merge_request_reviewer_spec.rb20
-rw-r--r--spec/models/merge_request_spec.rb95
-rw-r--r--spec/models/ml/candidate_spec.rb31
-rw-r--r--spec/models/ml/experiment_spec.rb51
-rw-r--r--spec/models/namespace_setting_spec.rb50
-rw-r--r--spec/models/namespace_spec.rb133
-rw-r--r--spec/models/namespaces/sync_event_spec.rb23
-rw-r--r--spec/models/note_spec.rb26
-rw-r--r--spec/models/notification_recipient_spec.rb50
-rw-r--r--spec/models/oauth_access_token_spec.rb17
-rw-r--r--spec/models/onboarding/completion_spec.rb (renamed from spec/lib/learn_gitlab/onboarding_spec.rb)24
-rw-r--r--spec/models/onboarding/learn_gitlab_spec.rb (renamed from spec/lib/learn_gitlab/project_spec.rb)20
-rw-r--r--spec/models/onboarding/progress_spec.rb (renamed from spec/models/onboarding_progress_spec.rb)52
-rw-r--r--spec/models/operations/feature_flag_spec.rb14
-rw-r--r--spec/models/packages/package_spec.rb13
-rw-r--r--spec/models/packages/rpm/metadatum_spec.rb36
-rw-r--r--spec/models/pages_domain_spec.rb58
-rw-r--r--spec/models/personal_access_token_spec.rb75
-rw-r--r--spec/models/pool_repository_spec.rb9
-rw-r--r--spec/models/preloaders/project_policy_preloader_spec.rb55
-rw-r--r--spec/models/preloaders/project_root_ancestor_preloader_spec.rb99
-rw-r--r--spec/models/project_setting_spec.rb47
-rw-r--r--spec/models/project_spec.rb257
-rw-r--r--spec/models/project_statistics_spec.rb30
-rw-r--r--spec/models/projects/build_artifacts_size_refresh_spec.rb12
-rw-r--r--spec/models/protected_branch_spec.rb33
-rw-r--r--spec/models/remote_mirror_spec.rb8
-rw-r--r--spec/models/repository_spec.rb77
-rw-r--r--spec/models/resource_state_event_spec.rb42
-rw-r--r--spec/models/snippet_repository_spec.rb8
-rw-r--r--spec/models/spam_log_spec.rb34
-rw-r--r--spec/models/user_spec.rb121
-rw-r--r--spec/models/user_status_spec.rb8
-rw-r--r--spec/models/users/credit_card_validation_spec.rb23
-rw-r--r--spec/models/users/ghost_user_migration_spec.rb14
-rw-r--r--spec/models/users/merge_request_interaction_spec.rb3
-rw-r--r--spec/models/users_star_project_spec.rb7
-rw-r--r--spec/models/work_item_spec.rb21
-rw-r--r--spec/models/work_items/widgets/description_spec.rb43
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb2
-rw-r--r--spec/policies/ci/runner_policy_spec.rb160
-rw-r--r--spec/policies/clusters/agent_policy_spec.rb2
-rw-r--r--spec/policies/commit_policy_spec.rb107
-rw-r--r--spec/policies/group_member_policy_spec.rb6
-rw-r--r--spec/policies/group_policy_spec.rb98
-rw-r--r--spec/policies/issuable_policy_spec.rb54
-rw-r--r--spec/policies/issue_policy_spec.rb2
-rw-r--r--spec/policies/merge_request_policy_spec.rb374
-rw-r--r--spec/policies/packages/policies/group_policy_spec.rb79
-rw-r--r--spec/policies/packages/policies/project_policy_spec.rb164
-rw-r--r--spec/policies/project_policy_spec.rb158
-rw-r--r--spec/policies/protected_branch_access_policy_spec.rb31
-rw-r--r--spec/policies/protected_branch_policy_spec.rb44
-rw-r--r--spec/policies/terraform/state_policy_spec.rb2
-rw-r--r--spec/policies/terraform/state_version_policy_spec.rb2
-rw-r--r--spec/presenters/blobs/notebook_presenter_spec.rb10
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb2
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb26
-rw-r--r--spec/presenters/deployments/deployment_presenter_spec.rb15
-rw-r--r--spec/presenters/packages/composer/packages_presenter_spec.rb2
-rw-r--r--spec/presenters/packages/conan/package_presenter_spec.rb2
-rw-r--r--spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb2
-rw-r--r--spec/presenters/project_presenter_spec.rb20
-rw-r--r--spec/requests/admin/hook_logs_controller_spec.rb15
-rw-r--r--spec/requests/api/admin/batched_background_migrations_spec.rb230
-rw-r--r--spec/requests/api/branches_spec.rb141
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb12
-rw-r--r--spec/requests/api/ci/jobs_spec.rb26
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb83
-rw-r--r--spec/requests/api/ci/runners_spec.rb38
-rw-r--r--spec/requests/api/commit_statuses_spec.rb20
-rw-r--r--spec/requests/api/commits_spec.rb616
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb6
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb6
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb40
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb40
-rw-r--r--spec/requests/api/deployments_spec.rb9
-rw-r--r--spec/requests/api/feature_flags_spec.rb8
-rw-r--r--spec/requests/api/files_spec.rb715
-rw-r--r--spec/requests/api/generic_packages_spec.rb18
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb93
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb12
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb31
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb163
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb18
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb12
-rw-r--r--spec/requests/api/graphql/environments/deployments_query_spec.rb487
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb9
-rw-r--r--spec/requests/api/graphql/group/packages_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/branches/create_spec.rb89
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb54
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb47
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/releases/update_spec.rb11
-rw-r--r--spec/requests/api/graphql/packages/composer_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/conan_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/helm_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/maven_spec.rb6
-rw-r--r--spec/requests/api/graphql/packages/nuget_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb15
-rw-r--r--spec/requests/api/graphql/packages/pypi_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb109
-rw-r--r--spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb109
-rw-r--r--spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb60
-rw-r--r--spec/requests/api/graphql/project/branch_rules_spec.rb122
-rw-r--r--spec/requests/api/graphql/project/deployment_spec.rb51
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb133
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb10
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb26
-rw-r--r--spec/requests/api/graphql/project/job_spec.rb54
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb20
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb16
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb18
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb63
-rw-r--r--spec/requests/api/graphql/query_spec.rb4
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb19
-rw-r--r--spec/requests/api/group_export_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb136
-rw-r--r--spec/requests/api/import_github_spec.rb10
-rw-r--r--spec/requests/api/integrations/slack/events_spec.rb112
-rw-r--r--spec/requests/api/internal/base_spec.rb201
-rw-r--r--spec/requests/api/internal/lfs_spec.rb26
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb8
-rw-r--r--spec/requests/api/markdown_snapshot_spec.rb4
-rw-r--r--spec/requests/api/maven_packages_spec.rb160
-rw-r--r--spec/requests/api/merge_requests_spec.rb187
-rw-r--r--spec/requests/api/ml/mlflow_spec.rb366
-rw-r--r--spec/requests/api/namespaces_spec.rb8
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb3
-rw-r--r--spec/requests/api/personal_access_tokens/self_revocation_spec.rb69
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb58
-rw-r--r--spec/requests/api/project_attributes.yml2
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/requests/api/project_packages_spec.rb13
-rw-r--r--spec/requests/api/project_snippets_spec.rb3
-rw-r--r--spec/requests/api/projects_spec.rb190
-rw-r--r--spec/requests/api/releases_spec.rb218
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb60
-rw-r--r--spec/requests/api/resource_state_events_spec.rb83
-rw-r--r--spec/requests/api/rpm_project_packages_spec.rb250
-rw-r--r--spec/requests/api/search_spec.rb40
-rw-r--r--spec/requests/api/settings_spec.rb14
-rw-r--r--spec/requests/api/snippets_spec.rb2
-rw-r--r--spec/requests/api/suggestions_spec.rb15
-rw-r--r--spec/requests/api/tags_spec.rb78
-rw-r--r--spec/requests/api/topics_spec.rb62
-rw-r--r--spec/requests/api/unleash_spec.rb94
-rw-r--r--spec/requests/api/usage_data_queries_spec.rb32
-rw-r--r--spec/requests/api/usage_data_spec.rb4
-rw-r--r--spec/requests/api/users_spec.rb122
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/requests/groups/observability_controller_spec.rb190
-rw-r--r--spec/requests/health_controller_spec.rb22
-rw-r--r--spec/requests/jira_connect/oauth_callbacks_controller_spec.rb6
-rw-r--r--spec/requests/jira_connect/subscriptions_controller_spec.rb21
-rw-r--r--spec/requests/jwt_controller_spec.rb24
-rw-r--r--spec/requests/oauth_tokens_spec.rb11
-rw-r--r--spec/requests/openid_connect_spec.rb20
-rw-r--r--spec/requests/projects/environments_controller_spec.rb4
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb44
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb221
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb72
-rw-r--r--spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb28
-rw-r--r--spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb14
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb42
-rw-r--r--spec/requests/projects/hook_logs_controller_spec.rb19
-rw-r--r--spec/requests/projects/merge_requests/context_commit_diffs_spec.rb26
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb283
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb9
-rw-r--r--spec/requests/projects/packages/package_files_controller_spec.rb30
-rw-r--r--spec/requests/projects/settings/integration_hook_logs_controller_spec.rb20
-rw-r--r--spec/requests/verifies_with_email_spec.rb14
-rw-r--r--spec/routing/group_routing_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb20
-rw-r--r--spec/routing/uploads_routing_spec.rb8
-rw-r--r--spec/rubocop/check_graceful_task_spec.rb125
-rw-r--r--spec/rubocop/code_reuse_helpers_spec.rb29
-rw-r--r--spec/rubocop/cop/active_model_errors_direct_manipulation_spec.rb4
-rw-r--r--spec/rubocop/cop/active_record_association_reload_spec.rb4
-rw-r--r--spec/rubocop/cop/api/base_spec.rb4
-rw-r--r--spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb4
-rw-r--r--spec/rubocop/cop/avoid_becomes_spec.rb4
-rw-r--r--spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb4
-rw-r--r--spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb4
-rw-r--r--spec/rubocop/cop/avoid_return_from_blocks_spec.rb4
-rw-r--r--spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb4
-rw-r--r--spec/rubocop/cop/ban_catch_throw_spec.rb4
-rw-r--r--spec/rubocop/cop/code_reuse/finder_spec.rb4
-rw-r--r--spec/rubocop/cop/code_reuse/presenter_spec.rb4
-rw-r--r--spec/rubocop/cop/code_reuse/serializer_spec.rb4
-rw-r--r--spec/rubocop/cop/code_reuse/service_class_spec.rb4
-rw-r--r--spec/rubocop/cop/code_reuse/worker_spec.rb4
-rw-r--r--spec/rubocop/cop/database/disable_referential_integrity_spec.rb4
-rw-r--r--spec/rubocop/cop/database/establish_connection_spec.rb4
-rw-r--r--spec/rubocop/cop/database/multiple_databases_spec.rb4
-rw-r--r--spec/rubocop/cop/database/rescue_query_canceled_spec.rb4
-rw-r--r--spec/rubocop/cop/database/rescue_statement_timeout_spec.rb4
-rw-r--r--spec/rubocop/cop/default_scope_spec.rb4
-rw-r--r--spec/rubocop/cop/destroy_all_spec.rb10
-rw-r--r--spec/rubocop/cop/file_decompression_spec.rb4
-rw-r--r--spec/rubocop/cop/filename_length_spec.rb4
-rw-r--r--spec/rubocop/cop/gemspec/avoid_executing_git_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/avoid_feature_category_not_owned_spec.rb10
-rw-r--r--spec/rubocop/cop/gitlab/avoid_feature_get_spec.rb32
-rw-r--r--spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/bulk_insert_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/change_timezone_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/deprecate_track_redis_hll_event_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/except_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/feature_available_usage_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/httparty_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/intersect_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/json_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/keys_first_and_values_first_spec.rb52
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb18
-rw-r--r--spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/namespaced_class_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/predicate_memoization_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/rails_logger_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/union_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/authorize_types_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/descriptions_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/gid_expected_type_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/graphql_name_position_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/id_type_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/json_type_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/old_types_spec.rb4
-rw-r--r--spec/rubocop/cop/graphql/resolver_type_spec.rb4
-rw-r--r--spec/rubocop/cop/group_public_or_visible_to_user_spec.rb4
-rw-r--r--spec/rubocop/cop/ignored_columns_spec.rb4
-rw-r--r--spec/rubocop/cop/include_sidekiq_worker_spec.rb4
-rw-r--r--spec/rubocop/cop/inject_enterprise_edition_module_spec.rb4
-rw-r--r--spec/rubocop/cop/lint/last_keyword_argument_spec.rb12
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_index_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/add_index_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/add_reference_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_timestamps_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/background_migration_base_class_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/background_migration_record_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/background_migrations_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/create_table_with_foreign_keys_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/datetime_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/drop_table_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/migration_record_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/prevent_index_creation_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/prevent_strings_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/remove_column_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/remove_concurrent_index_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/remove_index_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/safer_boolean_column_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/schedule_async_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/timestamps_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/update_column_in_batches_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/versioned_migration_class_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb4
-rw-r--r--spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb4
-rw-r--r--spec/rubocop/cop/performance/active_record_subtransactions_spec.rb4
-rw-r--r--spec/rubocop/cop/performance/ar_count_each_spec.rb8
-rw-r--r--spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb8
-rw-r--r--spec/rubocop/cop/performance/readlines_each_spec.rb4
-rw-r--r--spec/rubocop/cop/prefer_class_methods_over_module_spec.rb4
-rw-r--r--spec/rubocop/cop/project_path_helper_spec.rb4
-rw-r--r--spec/rubocop/cop/put_group_routes_under_scope_spec.rb4
-rw-r--r--spec/rubocop/cop/put_project_routes_under_scope_spec.rb4
-rw-r--r--spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb4
-rw-r--r--spec/rubocop/cop/qa/element_with_pattern_spec.rb4
-rw-r--r--spec/rubocop/cop/qa/selector_usage_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/any_instance_of_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/be_success_matcher_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/env_assignment_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/timecop_freeze_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/timecop_travel_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/top_level_describe_path_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/web_mock_enable_spec.rb4
-rw-r--r--spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb4
-rw-r--r--spec/rubocop/cop/safe_params_spec.rb4
-rw-r--r--spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb4
-rw-r--r--spec/rubocop/cop/scalability/cron_worker_context_spec.rb4
-rw-r--r--spec/rubocop/cop/scalability/file_uploads_spec.rb4
-rw-r--r--spec/rubocop/cop/scalability/idempotent_worker_spec.rb4
-rw-r--r--spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_spec.rb4
-rw-r--r--spec/rubocop/cop/sidekiq_options_queue_spec.rb4
-rw-r--r--spec/rubocop/cop/static_translation_definition_spec.rb4
-rw-r--r--spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb2
-rw-r--r--spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb4
-rw-r--r--spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb4
-rw-r--r--spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb4
-rw-r--r--spec/rubocop/cop/usage_data/large_table_spec.rb4
-rw-r--r--spec/rubocop/cop/user_admin_spec.rb4
-rw-r--r--spec/rubocop/cop_todo_spec.rb22
-rw-r--r--spec/rubocop/formatter/graceful_formatter_spec.rb239
-rw-r--r--spec/rubocop/formatter/todo_formatter_spec.rb96
-rw-r--r--spec/rubocop/qa_helpers_spec.rb2
-rw-r--r--spec/rubocop/todo_dir_spec.rb4
-rw-r--r--spec/rubocop_spec_helper.rb20
-rw-r--r--spec/scripts/changed-feature-flags_spec.rb1
-rw-r--r--spec/scripts/determine-qa-tests_spec.rb109
-rw-r--r--spec/scripts/lib/glfm/parse_examples_spec.rb331
-rw-r--r--spec/scripts/lib/glfm/shared_spec.rb18
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb500
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb21
-rw-r--r--spec/scripts/trigger-build_spec.rb286
-rw-r--r--spec/serializers/access_token_entity_base_spec.rb26
-rw-r--r--spec/serializers/ci/dag_pipeline_entity_spec.rb32
-rw-r--r--spec/serializers/ci/lint/job_entity_spec.rb2
-rw-r--r--spec/serializers/cluster_entity_spec.rb2
-rw-r--r--spec/serializers/container_repository_entity_spec.rb3
-rw-r--r--spec/serializers/deployment_entity_spec.rb3
-rw-r--r--spec/serializers/group_access_token_entity_spec.rb4
-rw-r--r--spec/serializers/impersonation_access_token_entity_spec.rb26
-rw-r--r--spec/serializers/impersonation_access_token_serializer_spec.rb20
-rw-r--r--spec/serializers/import/provider_repo_serializer_spec.rb2
-rw-r--r--spec/serializers/member_user_entity_spec.rb6
-rw-r--r--spec/serializers/merge_request_metrics_helper_spec.rb6
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb2
-rw-r--r--spec/serializers/project_access_token_entity_spec.rb4
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb6
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/tree_export_service_spec.rb4
-rw-r--r--spec/services/ci/after_requeue_job_service_spec.rb97
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb9
-rw-r--r--spec/services/ci/build_erase_service_spec.rb88
-rw-r--r--spec/services/ci/compare_reports_base_service_spec.rb18
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb67
-rw-r--r--spec/services/ci/create_pipeline_service/artifacts_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/cache_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/custom_config_content_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/dry_run_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/environment_spec.rb48
-rw-r--r--spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/include_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/logger_spec.rb8
-rw-r--r--spec/services/ci/create_pipeline_service/merge_requests_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/needs_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parallel_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parameter_content_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb4
-rw-r--r--spec/services/ci/create_pipeline_service/partitioning_spec.rb146
-rw-r--r--spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/rate_limit_spec.rb4
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/tags_spec.rb4
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb17
-rw-r--r--spec/services/ci/job_artifacts/delete_service_spec.rb41
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb11
-rw-r--r--spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb122
-rw-r--r--spec/services/ci/job_token_scope/add_project_service_spec.rb20
-rw-r--r--spec/services/ci/job_token_scope/remove_project_service_spec.rb20
-rw-r--r--spec/services/ci/list_config_variables_service_spec.rb4
-rw-r--r--spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb24
-rw-r--r--spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb24
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb27
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_1.yml55
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_2.yml63
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_subsequent_manual_jobs.yml65
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb11
-rw-r--r--spec/services/ci/pipelines/add_job_service_spec.rb8
-rw-r--r--spec/services/ci/pipelines/hook_service_spec.rb2
-rw-r--r--spec/services/ci/play_manual_stage_service_spec.rb2
-rw-r--r--spec/services/ci/process_sync_events_service_spec.rb12
-rw-r--r--spec/services/ci/queue/pending_builds_strategy_spec.rb24
-rw-r--r--spec/services/ci/register_job_service_spec.rb8
-rw-r--r--spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb15
-rw-r--r--spec/services/ci/retry_job_service_spec.rb52
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb121
-rw-r--r--spec/services/ci/runners/set_runner_associated_projects_service_spec.rb88
-rw-r--r--spec/services/ci/runners/update_runner_service_spec.rb80
-rw-r--r--spec/services/ci/unlock_artifacts_service_spec.rb34
-rw-r--r--spec/services/container_expiration_policies/cleanup_service_spec.rb2
-rw-r--r--spec/services/deployments/link_merge_requests_service_spec.rb4
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb27
-rw-r--r--spec/services/design_management/delete_designs_service_spec.rb8
-rw-r--r--spec/services/design_management/save_designs_service_spec.rb16
-rw-r--r--spec/services/discussions/capture_diff_note_positions_service_spec.rb4
-rw-r--r--spec/services/environments/stop_service_spec.rb36
-rw-r--r--spec/services/event_create_service_spec.rb12
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb2
-rw-r--r--spec/services/git/branch_push_service_spec.rb8
-rw-r--r--spec/services/git/wiki_push_service_spec.rb32
-rw-r--r--spec/services/google_cloud/enable_cloudsql_service_spec.rb22
-rw-r--r--spec/services/google_cloud/fetch_google_ip_list_service_spec.rb73
-rw-r--r--spec/services/groups/create_service_spec.rb4
-rw-r--r--spec/services/groups/destroy_service_spec.rb39
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb28
-rw-r--r--spec/services/import/github_service_spec.rb10
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb60
-rw-r--r--spec/services/issues/relative_position_rebalancing_service_spec.rb15
-rw-r--r--spec/services/issues/update_service_spec.rb37
-rw-r--r--spec/services/members/create_service_spec.rb26
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb73
-rw-r--r--spec/services/merge_requests/build_service_spec.rb24
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb8
-rw-r--r--spec/services/merge_requests/handle_assignees_change_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb97
-rw-r--r--spec/services/merge_requests/mergeability/logger_spec.rb121
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb10
-rw-r--r--spec/services/merge_requests/update_assignees_service_spec.rb43
-rw-r--r--spec/services/merge_requests/update_service_spec.rb111
-rw-r--r--spec/services/metrics/dashboard/clone_dashboard_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb10
-rw-r--r--spec/services/notes/destroy_service_spec.rb11
-rw-r--r--spec/services/onboarding/progress_service_spec.rb (renamed from spec/services/onboarding_progress_service_spec.rb)16
-rw-r--r--spec/services/packages/debian/parse_debian822_service_spec.rb2
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb6
-rw-r--r--spec/services/packages/rpm/repository_metadata/base_builder_spec.rb22
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb66
-rw-r--r--spec/services/post_receive_service_spec.rb3
-rw-r--r--spec/services/projects/blame_service_spec.rb12
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb456
-rw-r--r--spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb183
-rw-r--r--spec/services/projects/create_service_spec.rb26
-rw-r--r--spec/services/projects/destroy_service_spec.rb43
-rw-r--r--spec/services/projects/update_pages_service_spec.rb19
-rw-r--r--spec/services/protected_branches/cache_service_spec.rb10
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb2
-rw-r--r--spec/services/protected_branches/update_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb57
-rw-r--r--spec/services/releases/create_service_spec.rb22
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb33
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb19
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb2
-rw-r--r--spec/services/service_ping/submit_service_ping_service_spec.rb151
-rw-r--r--spec/services/service_response_spec.rb15
-rw-r--r--spec/services/snippets/bulk_destroy_service_spec.rb4
-rw-r--r--spec/services/spam/spam_action_service_spec.rb78
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb78
-rw-r--r--spec/services/suggestions/apply_service_spec.rb53
-rw-r--r--spec/services/system_notes/time_tracking_service_spec.rb91
-rw-r--r--spec/services/topics/merge_service_spec.rb10
-rw-r--r--spec/services/users/destroy_service_spec.rb586
-rw-r--r--spec/services/users/email_verification/generate_token_service_spec.rb37
-rw-r--r--spec/services/users/email_verification/validate_token_service_spec.rb97
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb31
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_service_spec.rb259
-rw-r--r--spec/services/users/reject_service_spec.rb26
-rw-r--r--spec/services/work_items/update_service_spec.rb12
-rw-r--r--spec/services/work_items/widgets/description_service/update_service_spec.rb2
-rw-r--r--spec/spec_helper.rb40
-rw-r--r--spec/support/capybara.rb6
-rw-r--r--spec/support/database/without_check_constraint.rb52
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci.yml3
-rw-r--r--spec/support/helpers/api_internal_base_helpers.rb16
-rw-r--r--spec/support/helpers/ci/template_helpers.rb4
-rw-r--r--spec/support/helpers/create_environments_helpers.rb4
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/database/partitioning_helpers.rb4
-rw-r--r--spec/support/helpers/gitlab_shell_helpers.rb14
-rw-r--r--spec/support/helpers/graphql_helpers.rb7
-rw-r--r--spec/support/helpers/html_escaped_helpers.rb24
-rw-r--r--spec/support/helpers/javascript_form_helper.rb7
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/migrations_helpers/work_item_types_helper.rb8
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb20
-rw-r--r--spec/support/helpers/seed_helper.rb4
-rw-r--r--spec/support/helpers/snowplow_helpers.rb4
-rw-r--r--spec/support/helpers/stub_configuration.rb4
-rw-r--r--spec/support/helpers/stub_object_storage.rb4
-rw-r--r--spec/support/helpers/test_env.rb163
-rw-r--r--spec/support/helpers/usage_data_helpers.rb43
-rw-r--r--spec/support/matchers/abort_matcher.rb19
-rw-r--r--spec/support/matchers/graphql_matchers.rb58
-rw-r--r--spec/support/matchers/markdown_matchers.rb12
-rw-r--r--spec/support/migrations_helpers/vulnerabilities_findings_helper.rb8
-rw-r--r--spec/support/redis.rb51
-rw-r--r--spec/support/redis/redis_helpers.rb34
-rw-r--r--spec/support/redis/redis_shared_examples.rb10
-rw-r--r--spec/support/rspec.rb26
-rw-r--r--spec/support/rspec_order.rb67
-rw-r--r--spec/support/rspec_order_todo.yml11150
-rw-r--r--spec/support/seed.rb9
-rw-r--r--spec/support/shared_contexts/bulk_imports_requests_shared_context.rb18
-rw-r--r--spec/support/shared_contexts/design_management_shared_contexts.rb18
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb22
-rw-r--r--spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb (renamed from spec/support/shared_contexts/markdown_snapshot_shared_examples.rb)35
-rw-r--r--spec/support/shared_contexts/glfm/example_snapshot_fixtures.rb27
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/markdown_golden_master_shared_examples.rb2
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb23
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb13
-rw-r--r--spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb28
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb14
-rw-r--r--spec/support/shared_contexts/views/html_safe_render_shared_context.rb39
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/controllers/error_tracking_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb2
-rw-r--r--spec/support/shared_examples/features/board_sidebar_labels_examples.rb2
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb121
-rw-r--r--spec/support/shared_examples/features/deploy_token_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb92
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb6
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/features/snippets_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/graphql/n_plus_one_query_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb99
-rw-r--r--spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/lib/cache_helpers_shared_examples.rb139
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb126
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/cluster_application_core_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/label_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb4
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/synthetic_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb162
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb16
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb150
-rw-r--r--spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb263
-rw-r--r--spec/support/shared_examples/quick_actions/incident/timeline_quick_action_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/labels_api_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/snippets_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/applications_controller_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/lfs_http_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb55
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb22
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb47
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/routing/resource_routing_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/routing/wiki_routing_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/security_training_providers_importer.rb14
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/services/common_system_notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/services/feature_flags/client_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/gitlab_projects_import_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/issuable/update_service_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/onboarding_progress_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/tasks/gitlab/uploads/migration_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb39
-rw-r--r--spec/support_specs/database/without_check_constraint_spec.rb85
-rw-r--r--spec/support_specs/helpers/html_escaped_helpers_spec.rb43
-rw-r--r--spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb157
-rw-r--r--spec/tasks/gitlab/db/validate_config_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/snippets_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb150
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb57
-rw-r--r--spec/tasks/rubocop_rake_spec.rb39
-rw-r--r--spec/tooling/danger/config_files_spec.rb91
-rw-r--r--spec/tooling/danger/datateam_spec.rb2
-rw-r--r--spec/tooling/danger/project_helper_spec.rb2
-rw-r--r--spec/uploaders/object_storage/cdn/google_cdn_spec.rb95
-rw-r--r--spec/uploaders/object_storage/cdn/google_ip_cache_spec.rb84
-rw-r--r--spec/uploaders/object_storage/cdn_spec.rb85
-rw-r--r--spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb4
-rw-r--r--spec/uploaders/packages/package_file_uploader_spec.rb71
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb162
-rw-r--r--spec/validators/addressable_url_validator_spec.rb4
-rw-r--r--spec/views/admin/sessions/new.html.haml_spec.rb8
-rw-r--r--spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb62
-rw-r--r--spec/views/devise/sessions/new.html.haml_spec.rb4
-rw-r--r--spec/views/devise/shared/_signup_box.html.haml_spec.rb10
-rw-r--r--spec/views/groups/new.html.haml_spec.rb11
-rw-r--r--spec/views/groups/observability.html.haml_spec.rb18
-rw-r--r--spec/views/help/drawers.html.haml_spec.rb18
-rw-r--r--spec/views/help/instance_configuration.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_header_search.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_published_experiments.html.haml_spec.rb6
-rw-r--r--spec/views/layouts/fullscreen.html.haml_spec.rb41
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb6
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb20
-rw-r--r--spec/views/notify/approved_merge_request_email.html.haml_spec.rb26
-rw-r--r--spec/views/notify/autodevops_disabled_email.text.erb_spec.rb2
-rw-r--r--spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb17
-rw-r--r--spec/views/notify/change_in_merge_request_draft_status_email.text.erb_spec.rb14
-rw-r--r--spec/views/notify/import_issues_csv_email.html.haml_spec.rb61
-rw-r--r--spec/views/profiles/keys/_form.html.haml_spec.rb4
-rw-r--r--spec/views/profiles/preferences/show.html.haml_spec.rb4
-rw-r--r--spec/views/profiles/show.html.haml_spec.rb5
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb56
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb2
-rw-r--r--spec/views/projects/settings/merge_requests/show.html.haml_spec.rb78
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb4
-rw-r--r--spec/views/shared/runners/_runner_details.html.haml_spec.rb4
-rw-r--r--spec/views/shared/web_hooks/_web_hook_disabled_alert.html.haml_spec.rb38
-rw-r--r--spec/workers/analytics/usage_trends/counter_job_worker_spec.rb34
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb2
-rw-r--r--spec/workers/ci/build_finished_worker_spec.rb13
-rw-r--r--spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb60
-rw-r--r--spec/workers/cleanup_container_repository_worker_spec.rb2
-rw-r--r--spec/workers/clusters/cleanup/project_namespace_worker_spec.rb2
-rw-r--r--spec/workers/concerns/application_worker_spec.rb2
-rw-r--r--spec/workers/concerns/cluster_agent_queue_spec.rb1
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb21
-rw-r--r--spec/workers/concerns/cronjob_queue_spec.rb4
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb18
-rw-r--r--spec/workers/concerns/pipeline_background_queue_spec.rb21
-rw-r--r--spec/workers/concerns/pipeline_queue_spec.rb21
-rw-r--r--spec/workers/concerns/repository_check_queue_spec.rb4
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb3
-rw-r--r--spec/workers/disallow_two_factor_for_group_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb2
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb6
-rw-r--r--spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb48
-rw-r--r--spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb49
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb58
-rw-r--r--spec/workers/gitlab_service_ping_worker_spec.rb21
-rw-r--r--spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb15
-rw-r--r--spec/workers/groups/update_two_factor_requirement_for_members_worker_spec.rb39
-rw-r--r--spec/workers/issues/close_worker_spec.rb86
-rw-r--r--spec/workers/namespaces/onboarding_issue_created_worker_spec.rb4
-rw-r--r--spec/workers/namespaces/process_sync_events_worker_spec.rb24
-rw-r--r--spec/workers/packages/helm/extraction_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb4
-rw-r--r--spec/workers/pages_worker_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb50
-rw-r--r--spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb92
-rw-r--r--spec/workers/projects/process_sync_events_worker_spec.rb8
-rw-r--r--spec/workers/purge_dependency_proxy_cache_worker_spec.rb4
-rw-r--r--spec/workers/releases/manage_evidence_worker_spec.rb2
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb23
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb4
-rw-r--r--spec/workers/ssh_keys/expired_notification_worker_spec.rb1
-rw-r--r--spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb1
-rw-r--r--spec/workers/users/deactivate_dormant_users_worker_spec.rb10
-rw-r--r--spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb53
2341 files changed, 60942 insertions, 19092 deletions
diff --git a/spec/benchmarks/banzai_benchmark.rb b/spec/benchmarks/banzai_benchmark.rb
index 86f7ee7e90b..7a60825c1e6 100644
--- a/spec/benchmarks/banzai_benchmark.rb
+++ b/spec/benchmarks/banzai_benchmark.rb
@@ -82,6 +82,11 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
it 'benchmarks all filters in the PlainMarkdownPipeline' do
benchmark_pipeline_filters(:plain_markdown)
end
+
+ it 'benchmarks specified filters in the FullPipeline' do
+ filter_klass_list = [Banzai::Filter::MathFilter]
+ benchmark_pipeline_filters(:full, filter_klass_list)
+ end
end
# build up the source text for each filter
@@ -105,7 +110,7 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
filter_source
end
- def benchmark_pipeline_filters(pipeline_type)
+ def benchmark_pipeline_filters(pipeline_type, filter_klass_list = nil)
pipeline = Banzai::Pipeline[pipeline_type]
filter_source = build_filter_text(pipeline, markdown_text)
@@ -114,7 +119,8 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
Benchmark.ips do |x|
x.config(time: 10, warmup: 2)
- pipeline.filters.each do |filter_klass|
+ filters = filter_klass_list || pipeline.filters
+ filters.each do |filter_klass|
label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20)
x.report(label) do
diff --git a/spec/components/layouts/horizontal_section_component_spec.rb b/spec/components/layouts/horizontal_section_component_spec.rb
new file mode 100644
index 00000000000..efc48213911
--- /dev/null
+++ b/spec/components/layouts/horizontal_section_component_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Layouts::HorizontalSectionComponent, type: :component do
+ let(:title) { 'Naming, visibility' }
+ let(:description) { 'Update your group name, description, avatar, and visibility.' }
+ let(:body) { 'This is where the settings go' }
+
+ describe 'slots' do
+ it 'renders title' do
+ render_inline described_class.new do |c|
+ c.title { title }
+ c.body { body }
+ end
+
+ expect(page).to have_css('h4', text: title)
+ end
+
+ it 'renders body slot' do
+ render_inline described_class.new do |c|
+ c.title { title }
+ c.body { body }
+ end
+
+ expect(page).to have_content(body)
+ end
+
+ context 'when description slot is provided' do
+ before do
+ render_inline described_class.new do |c|
+ c.title { title }
+ c.description { description }
+ c.body { body }
+ end
+ end
+
+ it 'renders description' do
+ expect(page).to have_css('p', text: description)
+ end
+ end
+
+ context 'when description slot is not provided' do
+ before do
+ render_inline described_class.new do |c|
+ c.title { title }
+ c.body { body }
+ end
+ end
+
+ it 'does not render description' do
+ expect(page).not_to have_css('p', text: description)
+ end
+ end
+ end
+
+ describe 'arguments' do
+ describe 'border' do
+ it 'defaults to true and adds gl-border-b CSS class' do
+ render_inline described_class.new do |c|
+ c.title { title }
+ c.body { body }
+ end
+
+ expect(page).to have_css('.gl-border-b')
+ end
+
+ it 'does not add gl-border-b CSS class when set to false' do
+ render_inline described_class.new(border: false) do |c|
+ c.title { title }
+ c.body { body }
+ end
+
+ expect(page).not_to have_css('.gl-border-b')
+ end
+ end
+
+ describe 'options' do
+ it 'adds options to wrapping element' do
+ render_inline described_class.new(options: { data: { testid: 'foo-bar' }, class: 'foo-bar' }) do |c|
+ c.title { title }
+ c.body { body }
+ end
+
+ expect(page).to have_css('.foo-bar[data-testid="foo-bar"]')
+ end
+ end
+ end
+end
diff --git a/spec/components/pajamas/badge_component_spec.rb b/spec/components/pajamas/badge_component_spec.rb
new file mode 100644
index 00000000000..4c564121ba2
--- /dev/null
+++ b/spec/components/pajamas/badge_component_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Pajamas::BadgeComponent, type: :component do
+ let(:text) { "Hello" }
+ let(:options) { {} }
+ let(:html_options) { {} }
+
+ before do
+ render_inline(described_class.new(text, **options, **html_options))
+ end
+
+ describe "text param" do
+ it "is shown inside the badge" do
+ expect(page).to have_css ".gl-badge", text: text
+ end
+ end
+
+ describe "content slot" do
+ it "can be used instead of the text param" do
+ render_inline(described_class.new) do
+ "Slot content"
+ end
+ expect(page).to have_css ".gl-badge", text: "Slot content"
+ end
+
+ it "takes presendence over the text param" do
+ render_inline(described_class.new(text)) do
+ "Slot wins."
+ end
+ expect(page).to have_css ".gl-badge", text: "Slot wins."
+ end
+ end
+
+ describe "options" do
+ describe "icon" do
+ let(:options) { { icon: :tanuki } }
+
+ it "adds the correct icon and margin" do
+ expect(page).to have_css ".gl-icon.gl-badge-icon.gl-mr-2[data-testid='tanuki-icon']"
+ end
+ end
+
+ describe "icon_classes" do
+ let(:options) { { icon: :tanuki, icon_classes: icon_classes } }
+
+ context "as string" do
+ let(:icon_classes) { "js-special-badge-icon js-extra-special" }
+
+ it "combines custom classes and component classes" do
+ expect(page).to have_css \
+ ".gl-icon.gl-badge-icon.gl-mr-2.js-special-badge-icon.js-extra-special[data-testid='tanuki-icon']"
+ end
+ end
+
+ context "as array" do
+ let(:icon_classes) { %w[js-special-badge-icon js-extra-special] }
+
+ it "combines custom classes and component classes" do
+ expect(page).to have_css \
+ ".gl-icon.gl-badge-icon.gl-mr-2.js-special-badge-icon.js-extra-special[data-testid='tanuki-icon']"
+ end
+ end
+ end
+
+ describe "icon_only" do
+ let(:options) { { icon: :tanuki, icon_only: true } }
+
+ it "adds no extra margin to the icon" do
+ expect(page).not_to have_css ".gl-icon.gl-mr-2"
+ end
+
+ it "adds the text as ARIA label" do
+ expect(page).to have_css ".gl-badge[aria-label='#{text}'][role='img']"
+ end
+ end
+
+ describe "href" do
+ let(:options) { { href: "/foo" } }
+
+ it "makes the a badge a link" do
+ expect(page).to have_link text, class: "gl-badge", href: "/foo"
+ end
+ end
+
+ describe "size" do
+ where(:size) { [:sm, :md, :lg] }
+
+ with_them do
+ let(:options) { { size: size } }
+
+ it "adds size class" do
+ expect(page).to have_css ".gl-badge.#{size}"
+ end
+ end
+
+ context "with unknown size" do
+ let(:options) { { size: :xxl } }
+
+ it "adds the default size class" do
+ expect(page).to have_css ".gl-badge.md"
+ end
+ end
+ end
+
+ describe "variant" do
+ where(:variant) { [:muted, :neutral, :info, :success, :warning, :danger] }
+
+ with_them do
+ let(:options) { { variant: variant } }
+
+ it "adds variant class" do
+ expect(page).to have_css ".gl-badge.badge-#{variant}"
+ end
+ end
+
+ context "with unknown variant" do
+ let(:options) { { variant: :foo } }
+
+ it "adds the default variant class" do
+ expect(page).to have_css ".gl-badge.badge-muted"
+ end
+ end
+ end
+ end
+
+ describe "HTML options" do
+ let(:html_options) { { id: "badge-33", data: { foo: "bar" } } }
+
+ it "get added as HTML attributes" do
+ expect(page).to have_css ".gl-badge#badge-33[data-foo='bar']"
+ end
+
+ it "can be combined with component options in no particular order" do
+ render_inline(described_class.new(text, id: "badge-34", variant: :success, data: { foo: "baz" }, size: :sm))
+ expect(page).to have_css ".gl-badge.badge-success.sm#badge-34[data-foo='baz']"
+ end
+
+ context "with custom CSS classes" do
+ let(:html_options) { { id: "badge-35", class: "js-special-badge" } }
+
+ it "combines custom classes and component classes" do
+ expect(page).to have_css ".gl-badge.js-special-badge#badge-35"
+ end
+ end
+ end
+end
diff --git a/spec/components/previews/layouts/horizontal_section_component_preview.rb b/spec/components/previews/layouts/horizontal_section_component_preview.rb
new file mode 100644
index 00000000000..cc7e8c8c2b1
--- /dev/null
+++ b/spec/components/previews/layouts/horizontal_section_component_preview.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Layouts
+ class HorizontalSectionComponentPreview < ViewComponent::Preview
+ # @param border toggle
+ # @param title text
+ # @param description text
+ # @param body text
+ def default(
+ border: true,
+ title: 'Naming, visibility',
+ description: 'Update your group name, description, avatar, and visibility.',
+ body: 'Settings fields here.'
+ )
+ render(::Layouts::HorizontalSectionComponent.new(border: border, options: { class: 'gl-mb-6 gl-pb-3' })) do |c|
+ c.title { title }
+ c.description { description }
+ c.body { body }
+ end
+ end
+ end
+end
diff --git a/spec/components/previews/pajamas/badge_component_preview.rb b/spec/components/previews/pajamas/badge_component_preview.rb
new file mode 100644
index 00000000000..e740a4a38aa
--- /dev/null
+++ b/spec/components/previews/pajamas/badge_component_preview.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Pajamas
+ class BadgeComponentPreview < ViewComponent::Preview
+ # Badge
+ # ---
+ #
+ # See its design reference [here](https://design.gitlab.com/components/badge).
+ #
+ # @param icon select [~, star-o, issue-closed, tanuki]
+ # @param icon_only toggle
+ # @param href url
+ # @param size select [sm, md, lg]
+ # @param text text
+ # @param variant select [muted, neutral, info, success, warning, danger]
+ def default(icon: :tanuki, icon_only: false, href: nil, size: :md, text: "Tanuki", variant: :muted)
+ render Pajamas::BadgeComponent.new(
+ text,
+ icon: icon,
+ icon_only: icon_only,
+ href: href,
+ size: size,
+ variant: variant
+ )
+ end
+
+ # Using the content slot
+ # ---
+ #
+ # Use the content slot instead of the `text` param when things get more complicated than a plain string.
+ # All other options (`icon`, `size`, etc.) work as usual.
+ def slot
+ render Pajamas::BadgeComponent.new(size: :lg, variant: :info) do
+ "!ereht olleh".reverse.capitalize
+ end
+ end
+
+ # Custom HTML attributes and icon classes
+ # ---
+ #
+ # Any extra options passed into the component are treated as HTML attributes.
+ # This makes adding data or an id easy.
+ #
+ # CSS classes provided with the `class:` option are combined with the component classes.
+ #
+ # It is also possible to set custom `icon_classes:`.
+ #
+ # The order in which you provide these keywords doesn't matter.
+ def custom
+ render Pajamas::BadgeComponent.new(
+ "I'm special.",
+ class: "js-special-badge",
+ data: { count: 1 },
+ icon: :tanuki,
+ icon_classes: ["js-special-badge-icon"],
+ id: "special-badge-22",
+ variant: :success
+ )
+ end
+ end
+end
diff --git a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
index b5f8d363d40..1984aff01db 100644
--- a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
+++ b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'aggregated metrics' do
expect(aggregated_metrics).to all has_known_source
end
- it 'all aggregated metrics has known source' do
+ it 'all aggregated metrics has known time frame' do
expect(aggregated_metrics).to all have_known_time_frame
end
@@ -66,7 +66,7 @@ RSpec.describe 'aggregated metrics' do
expect(aggregate[:time_frame]).not_to include(Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME)
end
- it "only refers to known events" do
+ it "only refers to known events", :skip do
expect(aggregate[:events]).to all be_known_event
end
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index 1555124fe03..8ddb5652dae 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -200,33 +200,6 @@ RSpec.describe ObjectStoreSettings do
expect(settings.external_diffs['object_store']).to be_nil
end
end
-
- context 'with legacy config and legacy background upload is enabled' do
- let(:legacy_settings) do
- {
- 'enabled' => true,
- 'remote_directory' => 'some-bucket',
- 'proxy_download' => false
- }
- end
-
- before do
- stub_env(ObjectStoreSettings::LEGACY_BACKGROUND_UPLOADS_ENV, 'lfs')
- settings.lfs['object_store'] = described_class.legacy_parse(legacy_settings, 'lfs')
- end
-
- it 'enables background_upload and disables direct_upload' do
- subject
-
- expect(settings.artifacts['object_store']).to be_nil
- expect(settings.lfs['object_store']['remote_directory']).to eq('some-bucket')
- expect(settings.lfs['object_store']['bucket_prefix']).to eq(nil)
- # Enable background_upload if the environment variable is available
- expect(settings.lfs['object_store']['direct_upload']).to eq(false)
- expect(settings.lfs['object_store']['background_upload']).to eq(true)
- expect(settings.external_diffs['object_store']).to be_nil
- end
- end
end
end
@@ -266,48 +239,6 @@ RSpec.describe ObjectStoreSettings do
expect(settings['remote_directory']).to eq 'gitlab'
expect(settings['bucket_prefix']).to eq 'artifacts'
end
-
- context 'legacy background upload environment variable is enabled' do
- before do
- stub_env(ObjectStoreSettings::LEGACY_BACKGROUND_UPLOADS_ENV, 'artifacts,lfs')
- end
-
- it 'enables background_upload and disables direct_upload' do
- original_settings = Settingslogic.new({
- 'enabled' => true,
- 'remote_directory' => 'artifacts'
- })
-
- settings = described_class.legacy_parse(original_settings, 'artifacts')
-
- expect(settings['enabled']).to be true
- expect(settings['direct_upload']).to be false
- expect(settings['background_upload']).to be true
- expect(settings['remote_directory']).to eq 'artifacts'
- expect(settings['bucket_prefix']).to eq nil
- end
- end
-
- context 'legacy background upload environment variable is enabled for other types' do
- before do
- stub_env(ObjectStoreSettings::LEGACY_BACKGROUND_UPLOADS_ENV, 'uploads,lfs')
- end
-
- it 'enables direct_upload and disables background_upload' do
- original_settings = Settingslogic.new({
- 'enabled' => true,
- 'remote_directory' => 'artifacts'
- })
-
- settings = described_class.legacy_parse(original_settings, 'artifacts')
-
- expect(settings['enabled']).to be true
- expect(settings['direct_upload']).to be true
- expect(settings['background_upload']).to be false
- expect(settings['remote_directory']).to eq 'artifacts'
- expect(settings['bucket_prefix']).to eq nil
- end
- end
end
describe '.split_bucket_prefix' do
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index 1de0e7e6c26..9b721d8cfca 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -116,7 +116,7 @@ RSpec.describe Settings do
describe '.cron_for_service_ping' do
it 'returns correct crontab for some manually calculated example' do
allow(Gitlab::CurrentSettings)
- .to receive(:uuid) { 'd9e2f4e8-db1f-4e51-b03d-f427e1965c4a'}
+ .to receive(:uuid) { 'd9e2f4e8-db1f-4e51-b03d-f427e1965c4a' }
expect(described_class.send(:cron_for_service_ping)).to eq('44 10 * * 4')
end
@@ -150,4 +150,30 @@ RSpec.describe Settings do
expect(Settings.encrypted('tmp/tests/test.enc').read).to be_empty
end
end
+
+ describe '.build_sidekiq_routing_rules' do
+ [
+ [nil, [['*', 'default']]],
+ [[], [['*', 'default']]],
+ [[['name=foobar', 'foobar']], [['name=foobar', 'foobar']]]
+ ].each do |input_rules, output_rules|
+ context "Given input routing_rules #{input_rules}" do
+ it "returns output routing_rules #{output_rules}" do
+ expect(described_class.send(:build_sidekiq_routing_rules, input_rules)).to eq(output_rules)
+ end
+ end
+ end
+ end
+
+ describe '.microsoft_graph_mailer' do
+ it 'defaults' do
+ expect(described_class.microsoft_graph_mailer.enabled).to be false
+ expect(described_class.microsoft_graph_mailer.user_id).to be_nil
+ expect(described_class.microsoft_graph_mailer.tenant).to be_nil
+ expect(described_class.microsoft_graph_mailer.client_id).to be_nil
+ expect(described_class.microsoft_graph_mailer.client_secret).to be_nil
+ expect(described_class.microsoft_graph_mailer.azure_ad_endpoint).to eq('https://login.microsoftonline.com')
+ expect(described_class.microsoft_graph_mailer.graph_endpoint).to eq('https://graph.microsoft.com')
+ end
+ end
end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index e02589ddc83..ab0cad989cb 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:admin) { create(:admin) }
- let(:user) { create(:user)}
+ let(:user) { create(:user) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
@@ -362,6 +362,17 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
expect(application_settings.reload.pipeline_limit_per_project_user_sha).to eq(25)
end
end
+
+ context 'invitation flow enforcement setting' do
+ let(:application_settings) { ApplicationSetting.current }
+
+ it 'updates invitation_flow_enforcement setting' do
+ put :update, params: { application_setting: { invitation_flow_enforcement: true } }
+
+ expect(response).to redirect_to(general_admin_application_settings_path)
+ expect(application_settings.reload.invitation_flow_enforcement).to eq(true)
+ end
+ end
end
describe 'PUT #reset_registration_token' do
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
index 6c423097e70..bf7707f177c 100644
--- a/spec/controllers/admin/applications_controller_spec.rb
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -39,17 +39,43 @@ RSpec.describe Admin::ApplicationsController do
end
describe 'POST #create' do
- it 'creates the application' do
- create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+ context 'with hash_oauth_secrets flag off' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
- expect do
- post :create, params: { doorkeeper_application: create_params }
- end.to change { Doorkeeper::Application.count }.by(1)
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- application = Doorkeeper::Application.last
+ application = Doorkeeper::Application.last
- expect(response).to redirect_to(admin_application_path(application))
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ expect(response).to redirect_to(admin_application_path(application))
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
+
+ context 'with hash_oauth_secrets flag on' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: true)
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
it 'renders the application form on errors' do
@@ -62,17 +88,43 @@ RSpec.describe Admin::ApplicationsController do
end
context 'when the params are for a confidential application' do
- it 'creates a confidential application' do
- create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
+ context 'with hash_oauth_secrets flag off' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
- expect do
- post :create, params: { doorkeeper_application: create_params }
- end.to change { Doorkeeper::Application.count }.by(1)
+ it 'creates a confidential application' do
+ create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
- application = Doorkeeper::Application.last
+ expect do
+ post :create, params: { doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- expect(response).to redirect_to(admin_application_path(application))
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ application = Doorkeeper::Application.last
+
+ expect(response).to redirect_to(admin_application_path(application))
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
+
+ context 'with hash_oauth_secrets flag on' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: true)
+ end
+
+ it 'creates a confidential application' do
+ create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
+
+ expect do
+ post :create, params: { doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
end
diff --git a/spec/controllers/admin/cohorts_controller_spec.rb b/spec/controllers/admin/cohorts_controller_spec.rb
index d271276a3e4..766073977c6 100644
--- a/spec/controllers/admin/cohorts_controller_spec.rb
+++ b/spec/controllers/admin/cohorts_controller_spec.rb
@@ -13,5 +13,17 @@ RSpec.describe Admin::CohortsController do
it_behaves_like 'tracking unique visits', :index do
let(:target_id) { 'i_analytics_cohorts' }
end
+
+ it_behaves_like 'Snowplow event tracking' do
+ subject { get :index }
+
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'perform_analytics_usage_action' }
+ let(:label) { 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly' }
+ let(:property) { 'i_analytics_cohorts' }
+ let(:namespace) { nil }
+ let(:project) { nil }
+ end
end
end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index fea59969400..9e852cb28dd 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe Admin::RunnersController do
context 'with update succeeding' do
before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
- expect(service).to receive(:update).with(anything).and_call_original
+ expect(service).to receive(:execute).with(anything).and_call_original
end
end
@@ -91,7 +91,7 @@ RSpec.describe Admin::RunnersController do
context 'with update failing' do
before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
- expect(service).to receive(:update).with(anything).and_return(false)
+ expect(service).to receive(:execute).with(anything).and_return(ServiceResponse.error(message: 'failure'))
end
end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 13038339d08..48221f496fb 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -27,13 +27,34 @@ RSpec.describe Admin::SpamLogsController do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'removes user and their spam logs when removing the user', :sidekiq_might_not_need_inline do
- delete :destroy, params: { id: first_spam.id, remove_user: true }
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates user removal', :sidekiq_inline do
+ expect do
+ delete :destroy, params: { id: first_spam.id, remove_user: true }
+ end.not_to change { SpamLog.count }
- expect(flash[:notice]).to eq "User #{user.username} was successfully removed."
- expect(response).to have_gitlab_http_status(:found)
- expect(SpamLog.count).to eq(0)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response).to have_gitlab_http_status(:found)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ ).to be_exists
+ expect(flash[:notice]).to eq("User #{user.username} was successfully removed.")
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'removes user and their spam logs when removing the user', :sidekiq_inline do
+ delete :destroy, params: { id: first_spam.id, remove_user: true }
+
+ expect(flash[:notice]).to eq "User #{user.username} was successfully removed."
+ expect(response).to have_gitlab_http_status(:found)
+ expect(SpamLog.count).to eq(0)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
end
diff --git a/spec/controllers/admin/topics_controller_spec.rb b/spec/controllers/admin/topics_controller_spec.rb
index 87093e0263b..111fdcc3be6 100644
--- a/spec/controllers/admin/topics_controller_spec.rb
+++ b/spec/controllers/admin/topics_controller_spec.rb
@@ -194,7 +194,7 @@ RSpec.describe Admin::TopicsController do
end
it 'renders a 400 error for identical topic ids' do
- post :merge, params: { source_topic_id: topic, target_topic_id: topic.id }
+ post :merge, params: { source_topic_id: topic.id, target_topic_id: topic.id }
expect(response).to have_gitlab_http_status(:bad_request)
expect { topic.reload }.not_to raise_error
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 515ad9daf36..682399f4dd9 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -73,51 +73,120 @@ RSpec.describe Admin::UsersController do
project.add_developer(user)
end
- it 'deletes user and ghosts their contributions' do
- delete :destroy, params: { id: user.username }, format: :json
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates user removal' do
+ delete :destroy, params: { id: user.username }, format: :json
- expect(response).to have_gitlab_http_status(:ok)
- expect(User.exists?(user.id)).to be_falsy
- expect(issue.reload.author).to be_ghost
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: false)
+ ).to be_exists
+ end
- it 'deletes the user and their contributions when hard delete is specified' do
- delete :destroy, params: { id: user.username, hard_delete: true }, format: :json
+ it 'initiates user removal and passes hard delete option' do
+ delete :destroy, params: { id: user.username, hard_delete: true }, format: :json
- expect(response).to have_gitlab_http_status(:ok)
- expect(User.exists?(user.id)).to be_falsy
- expect(Issue.exists?(issue.id)).to be_falsy
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)
+ ).to be_exists
+ end
- context 'prerequisites for account deletion' do
- context 'solo-owned groups' do
- let(:group) { create(:group) }
+ context 'prerequisites for account deletion' do
+ context 'solo-owned groups' do
+ let(:group) { create(:group) }
- context 'if the user is the sole owner of at least one group' do
- before do
- create(:group_member, :owner, group: group, user: user)
- end
+ context 'if the user is the sole owner of at least one group' do
+ before do
+ create(:group_member, :owner, group: group, user: user)
+ end
+
+ context 'soft-delete' do
+ it 'fails' do
+ delete :destroy, params: { id: user.username }
- context 'soft-delete' do
- it 'fails' do
- delete :destroy, params: { id: user.username }
+ message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
- message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
+ expect(flash[:alert]).to eq(message)
+ expect(response).to have_gitlab_http_status(:see_other)
+ expect(response).to redirect_to admin_user_path(user)
+ expect(Users::GhostUserMigration).not_to exist
+ end
+ end
- expect(flash[:alert]).to eq(message)
- expect(response).to have_gitlab_http_status(:see_other)
- expect(response).to redirect_to admin_user_path(user)
- expect(User.exists?(user.id)).to be_truthy
+ context 'hard-delete' do
+ it 'succeeds' do
+ delete :destroy, params: { id: user.username, hard_delete: true }
+
+ expect(response).to redirect_to(admin_users_path)
+ expect(flash[:notice]).to eq(_('The user is being deleted.'))
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)
+ ).to be_exists
+ end
end
end
+ end
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'deletes user and ghosts their contributions' do
+ delete :destroy, params: { id: user.username }, format: :json
- context 'hard-delete' do
- it 'succeeds' do
- delete :destroy, params: { id: user.username, hard_delete: true }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(User.exists?(user.id)).to be_falsy
+ expect(issue.reload.author).to be_ghost
+ end
+
+ it 'deletes the user and their contributions when hard delete is specified' do
+ delete :destroy, params: { id: user.username, hard_delete: true }, format: :json
- expect(response).to redirect_to(admin_users_path)
- expect(flash[:notice]).to eq(_('The user is being deleted.'))
- expect(User.exists?(user.id)).to be_falsy
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(User.exists?(user.id)).to be_falsy
+ expect(Issue.exists?(issue.id)).to be_falsy
+ end
+
+ context 'prerequisites for account deletion' do
+ context 'solo-owned groups' do
+ let(:group) { create(:group) }
+
+ context 'if the user is the sole owner of at least one group' do
+ before do
+ create(:group_member, :owner, group: group, user: user)
+ end
+
+ context 'soft-delete' do
+ it 'fails' do
+ delete :destroy, params: { id: user.username }
+
+ message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
+
+ expect(flash[:alert]).to eq(message)
+ expect(response).to have_gitlab_http_status(:see_other)
+ expect(response).to redirect_to admin_user_path(user)
+ expect(User.exists?(user.id)).to be_truthy
+ end
+ end
+
+ context 'hard-delete' do
+ it 'succeeds' do
+ delete :destroy, params: { id: user.username, hard_delete: true }
+
+ expect(response).to redirect_to(admin_users_path)
+ expect(flash[:notice]).to eq(_('The user is being deleted.'))
+ expect(User.exists?(user.id)).to be_falsy
+ end
end
end
end
@@ -131,10 +200,27 @@ RSpec.describe Admin::UsersController do
context 'when rejecting a pending user' do
let(:user) { create(:user, :blocked_pending_approval) }
- it 'hard deletes the user', :sidekiq_inline do
- subject
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates user removal', :sidekiq_inline do
+ subject
- expect(User.exists?(user.id)).to be_falsy
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ ).to be_exists
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'hard deletes the user', :sidekiq_inline do
+ subject
+
+ expect(User.exists?(user.id)).to be_falsy
+ end
end
it 'displays the rejection message' do
@@ -270,19 +356,19 @@ RSpec.describe Admin::UsersController do
let(:user) { create(:user, **activity) }
context 'with no recent activity' do
- let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
+ let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago } }
it_behaves_like 'a request that deactivates the user'
end
context 'with recent activity' do
- let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
+ let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago } }
it 'does not deactivate the user' do
put :deactivate, params: { id: user.username }
user.reload
expect(user.deactivated?).to be_falsey
- expect(flash[:notice]).to eq("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
+ expect(flash[:notice]).to eq("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
end
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 1e28ef4ba93..f1adb9020fa 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1006,7 +1006,7 @@ RSpec.describe ApplicationController do
end
describe '.endpoint_id_for_action' do
- controller(described_class) { }
+ controller(described_class) {}
it 'returns an expected endpoint id' do
expect(controller.class.endpoint_id_for_action('hello')).to eq('AnonymousController#hello')
diff --git a/spec/controllers/concerns/continue_params_spec.rb b/spec/controllers/concerns/continue_params_spec.rb
index c010e8ffbd0..ba600b8156a 100644
--- a/spec/controllers/concerns/continue_params_spec.rb
+++ b/spec/controllers/concerns/continue_params_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe ContinueParams do
let(:controller_class) do
+ # rubocop:disable Rails/ApplicationController
Class.new(ActionController::Base) do
include ContinueParams
@@ -11,6 +12,7 @@ RSpec.describe ContinueParams do
@request ||= Struct.new(:host, :port).new('test.host', 80)
end
end
+ # rubocop:enable Rails/ApplicationController
end
subject(:controller) { controller_class.new }
diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb
index 250cc3cf2cf..2e734d81ea0 100644
--- a/spec/controllers/concerns/product_analytics_tracking_spec.rb
+++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe ProductAnalyticsTracking, :snowplow do
skip_before_action :authenticate_user!, only: :show
track_event(:index, :show, name: 'g_analytics_valuestream', destinations: [:redis_hll, :snowplow],
- conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id }
+ conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id }
def index
render html: 'index'
diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb
index 178684ae2d0..0ad8fa79e5e 100644
--- a/spec/controllers/concerns/redis_tracking_spec.rb
+++ b/spec/controllers/concerns/redis_tracking_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe RedisTracking do
include RedisTracking
skip_before_action :authenticate_user!, only: :show
- track_redis_hll_event(:index, :show, name: 'g_compliance_approval_rules',
+ track_redis_hll_event(:index, :show,
+ name: 'g_compliance_approval_rules',
if: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id }
def index
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
index 5b137ada141..111bfb24c7e 100644
--- a/spec/controllers/confirmations_controller_spec.rb
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -129,6 +129,10 @@ RSpec.describe ConfirmationsController do
subject(:perform_request) { post(:create, params: { user: { email: user.email } }) }
+ before do
+ stub_feature_flags(identity_verification: false)
+ end
+
context 'when reCAPTCHA is disabled' do
before do
stub_application_setting(recaptcha_enabled: false)
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 1d2f1085d3c..7c9236704ec 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -88,10 +88,11 @@ RSpec.describe GraphqlController do
post :execute, params: { _json: multiplex }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([
- { 'data' => { '__typename' => 'Query' } },
- { 'data' => { '__typename' => 'Query' } }
- ])
+ expect(json_response).to eq(
+ [
+ { 'data' => { '__typename' => 'Query' } },
+ { 'data' => { '__typename' => 'Query' } }
+ ])
end
it 'sets a limit on the total query size' do
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c6fd184ede0..a3659ae9163 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -97,6 +97,25 @@ RSpec.describe Groups::GroupMembersController do
expect(assigns(:members).map(&:user_id)).to contain_exactly(user.id)
end
end
+
+ context 'when webui_members_inherited_users is disabled' do
+ let_it_be(:shared_group) { create(:group) }
+ let_it_be(:shared_group_user) { create(:user) }
+ let_it_be(:group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
+
+ before do
+ group.add_owner(user)
+ shared_group.add_owner(shared_group_user)
+ stub_feature_flags(webui_members_inherited_users: false)
+ sign_in(user)
+ end
+
+ it 'lists inherited group members only' do
+ get :index, params: { group_id: shared_group }
+
+ expect(assigns(:members).map(&:user_id)).to contain_exactly(shared_group_user.id)
+ end
+ end
end
describe 'PUT update' do
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index 90da40cd5f0..37db26096d3 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Groups::LabelsController do
it 'returns group and project labels by default' do
get :index, params: { group_id: group }, format: :json
- label_ids = json_response.map {|label| label['title']}
+ label_ids = json_response.map { |label| label['title'] }
expect(label_ids).to match_array([label_1.title, group_label_1.title])
end
@@ -36,7 +36,7 @@ RSpec.describe Groups::LabelsController do
params = { group_id: subgroup, only_group_labels: true }
get :index, params: params, format: :json
- label_ids = json_response.map {|label| label['title']}
+ label_ids = json_response.map { |label| label['title'] }
expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
end
end
diff --git a/spec/controllers/groups/releases_controller_spec.rb b/spec/controllers/groups/releases_controller_spec.rb
index 9d372114d62..7dd0bc6206a 100644
--- a/spec/controllers/groups/releases_controller_spec.rb
+++ b/spec/controllers/groups/releases_controller_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Groups::ReleasesController do
end
it 'does not return any releases' do
- expect(json_response.map {|r| r['tag'] } ).to be_empty
+ expect(json_response.map { |r| r['tag'] } ).to be_empty
end
it 'returns OK' do
@@ -56,7 +56,7 @@ RSpec.describe Groups::ReleasesController do
index
- expect(json_response.map {|r| r['tag'] } ).to match_array(%w(p2 p1 v2 v1))
+ expect(json_response.map { |r| r['tag'] } ).to match_array(%w(p2 p1 v2 v1))
end
end
diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb
index 0804a5536e0..b9457770ed6 100644
--- a/spec/controllers/groups/settings/applications_controller_spec.rb
+++ b/spec/controllers/groups/settings/applications_controller_spec.rb
@@ -71,17 +71,43 @@ RSpec.describe Groups::Settings::ApplicationsController do
group.add_owner(user)
end
- it 'creates the application' do
- create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
+ context 'with hash_oauth_secrets flag on' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: true)
+ end
- expect do
- post :create, params: { group_id: group, doorkeeper_application: create_params }
- end.to change { Doorkeeper::Application.count }.by(1)
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- application = Doorkeeper::Application.last
+ application = Doorkeeper::Application.last
- expect(response).to redirect_to(group_settings_application_path(group, application))
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
+
+ context 'with hash_oauth_secrets flag off' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
it 'renders the application form on errors' do
@@ -94,17 +120,43 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when the params are for a confidential application' do
- it 'creates a confidential application' do
- create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
+ context 'with hash_oauth_secrets flag off' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
- expect do
- post :create, params: { group_id: group, doorkeeper_application: create_params }
- end.to change { Doorkeeper::Application.count }.by(1)
+ it 'creates a confidential application' do
+ create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
- application = Doorkeeper::Application.last
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- expect(response).to redirect_to(group_settings_application_path(group, application))
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ application = Doorkeeper::Application.last
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
+
+ context 'with hash_oauth_secrets flag on' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: true)
+ end
+
+ it 'creates a confidential application' do
+ create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
+
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 26e65711e9f..2375146f346 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -139,6 +139,39 @@ RSpec.describe HelpController do
end
end
+ describe 'GET #drawers' do
+ subject { get :drawers, params: { markdown_file: path } }
+
+ context 'when requested file exists' do
+ let(:path) { 'user/ssh' }
+ let(:file_name) { "#{path}.md" }
+
+ before do
+ subject
+ end
+
+ it 'assigns variables', :aggregate_failures do
+ expect(assigns[:path]).not_to be_empty
+ expect(assigns[:clean_path]).not_to be_empty
+ end
+
+ it 'renders HTML', :aggregate_failures do
+ is_expected.to render_template('help/drawers')
+ expect(response.media_type).to eq 'text/html'
+ end
+ end
+
+ context 'when requested file is missing' do
+ let(:path) { 'foo/bar' }
+
+ it 'renders not found' do
+ subject
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
describe 'GET #show' do
context 'for Markdown formats' do
subject { get :show, params: { path: path }, format: :md }
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index af220e2d515..e73e61b6ec5 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -49,10 +49,10 @@ RSpec.describe Import::BitbucketController do
let(:expires_in) { 1.day }
let(:access_token) do
double(token: token,
- secret: secret,
- expires_at: expires_at,
- expires_in: expires_in,
- refresh_token: refresh_token)
+ secret: secret,
+ expires_at: expires_at,
+ expires_in: expires_in,
+ refresh_token: refresh_token)
end
before do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 46160aac0c1..269eb62cae6 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -134,7 +134,7 @@ RSpec.describe Import::GithubController do
it 'fetches repos using legacy client' do
expect_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
- expect(client).to receive(:repos)
+ expect(client).to receive(:repos).and_return([])
end
get :status
@@ -164,8 +164,8 @@ RSpec.describe Import::GithubController do
end
it 'fetches repos using latest github client' do
- expect_next_instance_of(Octokit::Client) do |client|
- expect(client).to receive(:repos).and_return([].to_enum)
+ expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
+ expect(client).to receive(:repos).and_return([])
end
get :status
@@ -184,8 +184,8 @@ RSpec.describe Import::GithubController do
context 'pagination' do
context 'when no page is specified' do
it 'requests first page' do
- expect_next_instance_of(Octokit::Client) do |client|
- expect(client).to receive(:repos).with(nil, { page: 1, per_page: 25 }).and_return([].to_enum)
+ expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
+ expect(client).to receive(:repos).with({ page: 1, per_page: 25 }).and_return([])
end
get :status
diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb
index 0111ad9501f..6f805b44e89 100644
--- a/spec/controllers/import/manifest_controller_spec.rb
+++ b/spec/controllers/import/manifest_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do
include ImportSpecHelper
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group)}
+ let_it_be(:group) { create(:group) }
before(:all) do
group.add_maintainer(user)
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index 5bf3b4c48bf..9b16dc9a463 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -113,11 +113,30 @@ RSpec.describe Oauth::ApplicationsController do
subject { post :create, params: oauth_params }
- it 'creates an application' do
- subject
+ context 'when hash_oauth_tokens flag set' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: true)
+ end
- expect(response).to have_gitlab_http_status(:found)
- expect(response).to redirect_to(oauth_application_path(Doorkeeper::Application.last))
+ it 'creates an application' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ end
+ end
+
+ context 'when hash_oauth_tokens flag not set' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
+
+ it 'creates an application' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(oauth_application_path(Doorkeeper::Application.last))
+ end
end
it 'redirects back to profile page if OAuth applications are disabled' do
diff --git a/spec/controllers/oauth/token_info_controller_spec.rb b/spec/controllers/oauth/token_info_controller_spec.rb
index b66fff4d4e9..3cd952d4935 100644
--- a/spec/controllers/oauth/token_info_controller_spec.rb
+++ b/spec/controllers/oauth/token_info_controller_spec.rb
@@ -24,12 +24,12 @@ RSpec.describe Oauth::TokenInfoController do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to eq(
- 'scope' => %w[api],
- 'scopes' => %w[api],
- 'created_at' => access_token.created_at.to_i,
- 'expires_in' => access_token.expires_in,
- 'application' => { 'uid' => application.uid },
- 'resource_owner_id' => access_token.resource_owner_id,
+ 'scope' => %w[api],
+ 'scopes' => %w[api],
+ 'created_at' => access_token.created_at.to_i,
+ 'expires_in' => access_token.expires_in,
+ 'application' => { 'uid' => application.uid },
+ 'resource_owner_id' => access_token.resource_owner_id,
'expires_in_seconds' => access_token.expires_in
)
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 9ecef8b7450..df5da29495e 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -406,7 +406,7 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
before do
stub_last_request_id(last_request_id)
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
- providers: [saml_config])
+ providers: [saml_config])
mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
request.env['devise.mapping'] = Devise.mappings[:user]
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
diff --git a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
index 0e531dbaf4b..99e9644da66 100644
--- a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
@@ -48,8 +48,8 @@ RSpec.describe Profiles::PersonalAccessTokensController do
end
it "only includes details of the active personal access token" do
- active_personal_access_tokens_detail = ::API::Entities::PersonalAccessTokenWithDetails
- .represent([active_personal_access_token])
+ active_personal_access_tokens_detail =
+ ::PersonalAccessTokenSerializer.new.represent([active_personal_access_token])
expect(assigns(:active_personal_access_tokens).to_json).to eq(active_personal_access_tokens_detail.to_json)
end
@@ -100,8 +100,8 @@ RSpec.describe Profiles::PersonalAccessTokensController do
get :index
first_token = assigns(:active_personal_access_tokens).first.as_json
- expect(first_token[:name]).to eq("Token1")
- expect(first_token[:expires_at]).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
+ expect(first_token['name']).to eq("Token1")
+ expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
end
it "orders tokens on id in case token has same expires_at" do
@@ -110,12 +110,12 @@ RSpec.describe Profiles::PersonalAccessTokensController do
get :index
first_token = assigns(:active_personal_access_tokens).first.as_json
- expect(first_token[:name]).to eq("Token3")
- expect(first_token[:expires_at]).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
+ expect(first_token['name']).to eq("Token3")
+ expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
second_token = assigns(:active_personal_access_tokens).second.as_json
- expect(second_token[:name]).to eq("Token1")
- expect(second_token[:expires_at]).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
+ expect(second_token['name']).to eq("Token1")
+ expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
end
end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 89185a8f856..aa92ff6be33 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -82,13 +82,17 @@ RSpec.describe ProfilesController, :request_store do
expect(ldap_user.location).to eq('City, Country')
end
- it 'allows setting a user status' do
+ it 'allows setting a user status', :freeze_time do
sign_in(user)
- put :update, params: { user: { status: { message: 'Working hard!', availability: 'busy' } } }
+ put(
+ :update,
+ params: { user: { status: { message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours' } } }
+ )
expect(user.reload.status.message).to eq('Working hard!')
expect(user.reload.status.availability).to eq('busy')
+ expect(user.reload.status.clear_status_after).to eq(8.hours.from_now)
expect(response).to have_gitlab_http_status(:found)
end
diff --git a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
index 8903592ba15..b9e569b1647 100644
--- a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
+++ b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
@@ -16,7 +16,6 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
end
before do
- stub_feature_flags(use_vsa_aggregated_tables: false)
sign_in(user)
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 958fcd4360c..263f488ddbf 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -488,7 +488,7 @@ RSpec.describe Projects::ArtifactsController do
context 'with regular branch' do
before do
pipeline.update!(ref: 'master',
- sha: project.commit('master').sha)
+ sha: project.commit('master').sha)
get :latest_succeeded, params: params_from_ref('master')
end
@@ -499,7 +499,7 @@ RSpec.describe Projects::ArtifactsController do
context 'with branch name containing slash' do
before do
pipeline.update!(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
+ sha: project.commit('improve/awesome').sha)
get :latest_succeeded, params: params_from_ref('improve/awesome')
end
@@ -510,7 +510,7 @@ RSpec.describe Projects::ArtifactsController do
context 'with branch name and path containing slashes' do
before do
pipeline.update!(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
+ sha: project.commit('improve/awesome').sha)
get :latest_succeeded, params: params_from_ref('improve/awesome', job.name, 'file/README.md')
end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index bf475f6135a..62a544bb3fc 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Projects::BlameController do
end
context "invalid branch, valid file" do
- let(:id) { 'invalid-branch/files/ruby/missing_file.rb'}
+ let(:id) { 'invalid-branch/files/ruby/missing_file.rb' }
it { is_expected.to respond_with(:not_found) }
end
diff --git a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
index 3c4376909f8..e5bffb7c265 100644
--- a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
+++ b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
@@ -59,12 +59,13 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
- expect(csv_response).to eq([
- %w[date group_name coverage],
- ['2020-03-09', 'rspec', '79.0'],
- ['2020-03-08', 'rspec', '77.0'],
- ['2019-12-10', 'karma', '81.0']
- ])
+ expect(csv_response).to eq(
+ [
+ %w[date group_name coverage],
+ ['2020-03-09', 'rspec', '79.0'],
+ ['2020-03-08', 'rspec', '77.0'],
+ ['2019-12-10', 'karma', '81.0']
+ ])
end
context 'when given date range spans more than 90 days' do
@@ -72,12 +73,13 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
let(:end_date) { '2020-03-09' }
it 'limits the result to 90 days from the given start_date' do
- expect(csv_response).to eq([
- %w[date group_name coverage],
- ['2020-03-09', 'rspec', '79.0'],
- ['2020-03-08', 'rspec', '77.0'],
- ['2019-12-10', 'karma', '81.0']
- ])
+ expect(csv_response).to eq(
+ [
+ %w[date group_name coverage],
+ ['2020-03-09', 'rspec', '79.0'],
+ ['2020-03-08', 'rspec', '77.0'],
+ ['2019-12-10', 'karma', '81.0']
+ ])
end
end
@@ -89,29 +91,8 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
it 'serves the results in JSON' do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([
- {
- 'group_name' => 'rspec',
- 'data' => [
- { 'date' => '2020-03-09', 'coverage' => 79.0 },
- { 'date' => '2020-03-08', 'coverage' => 77.0 }
- ]
- },
- {
- 'group_name' => 'karma',
- 'data' => [
- { 'date' => '2019-12-10', 'coverage' => 81.0 }
- ]
- }
- ])
- end
-
- context 'when given date range spans more than 90 days' do
- let(:start_date) { '2019-12-09' }
- let(:end_date) { '2020-03-09' }
-
- it 'limits the result to 90 days from the given start_date' do
- expect(json_response).to eq([
+ expect(json_response).to eq(
+ [
{
'group_name' => 'rspec',
'data' => [
@@ -126,6 +107,29 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
]
}
])
+ end
+
+ context 'when given date range spans more than 90 days' do
+ let(:start_date) { '2019-12-09' }
+ let(:end_date) { '2020-03-09' }
+
+ it 'limits the result to 90 days from the given start_date' do
+ expect(json_response).to eq(
+ [
+ {
+ 'group_name' => 'rspec',
+ 'data' => [
+ { 'date' => '2020-03-09', 'coverage' => 79.0 },
+ { 'date' => '2020-03-08', 'coverage' => 77.0 }
+ ]
+ },
+ {
+ 'group_name' => 'karma',
+ 'data' => [
+ { 'date' => '2019-12-10', 'coverage' => 81.0 }
+ ]
+ }
+ ])
end
end
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index d778739be38..403f99145fc 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -143,10 +143,11 @@ RSpec.describe Projects::Ci::LintsController do
it_behaves_like 'returns a successful validation'
it 'assigns result with errors' do
- expect(parsed_body['errors']).to match_array([
- 'jobs rubocop config should implement a script: or a trigger: keyword',
- 'jobs config should contain at least one visible job'
- ])
+ expect(parsed_body['errors']).to match_array(
+ [
+ 'jobs rubocop config should implement a script: or a trigger: keyword',
+ 'jobs config should contain at least one visible job'
+ ])
end
context 'with dry_run mode' do
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index ccd213fdffa..f5dd8abd67b 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -30,6 +30,18 @@ RSpec.describe Projects::CycleAnalyticsController do
let(:request_params) { { namespace_id: project.namespace, project_id: project } }
let(:target_id) { 'p_analytics_valuestream' }
end
+
+ it_behaves_like 'Snowplow event tracking' do
+ subject { get :show, params: request_params, format: :html }
+
+ let(:request_params) { { namespace_id: project.namespace, project_id: project } }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'perform_analytics_usage_action' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly' }
+ let(:property) { 'p_analytics_valuestream' }
+ end
end
include_examples GracefulTimeoutHandling
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index 821f7fca73d..308146ce792 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe Projects::DeployKeysController do
end
context 'when json requested' do
- let(:project2) { create(:project, :internal)}
- let(:project_private) { create(:project, :private)}
+ let(:project2) { create(:project, :internal) }
+ let(:project_private) { create(:project, :private) }
let(:deploy_key_internal) { create(:deploy_key) }
let(:deploy_key_actual) { create(:deploy_key) }
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 1a6edab795d..16a43bae674 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -32,6 +32,11 @@ RSpec.describe Projects::EnvironmentsController do
get :index, params: environment_params
end
+
+ it_behaves_like 'tracking unique visits', :index do
+ let(:request_params) { environment_params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context 'when requesting JSON response for folders' do
@@ -68,6 +73,24 @@ RSpec.describe Projects::EnvironmentsController do
expect(json_response['stopped_count']).to eq 1
end
+ it 'handles search option properly' do
+ get :index, params: environment_params(format: :json, search: 'staging/r')
+
+ expect(environments.map { |env| env['name'] } ).to contain_exactly('staging/review-1', 'staging/review-2')
+ expect(json_response['available_count']).to eq 2
+ expect(json_response['stopped_count']).to eq 1
+ end
+
+ it 'ignores search option if is shorter than a minimum' do
+ get :index, params: environment_params(format: :json, search: 'st')
+
+ expect(environments.map { |env| env['name'] } ).to contain_exactly('production',
+ 'staging/review-1',
+ 'staging/review-2')
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
+
it 'sets the polling interval header' do
subject
@@ -149,29 +172,43 @@ RSpec.describe Projects::EnvironmentsController do
end
describe 'GET folder' do
- before do
- create(:environment, project: project,
- name: 'staging-1.0/review',
- state: :available)
- create(:environment, project: project,
- name: 'staging-1.0/zzz',
- state: :available)
- end
-
context 'when using default format' do
it 'responds with HTML' do
get :folder, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: 'staging-1.0'
- }
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'staging-1.0'
+ }
- expect(response).to be_ok
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template 'folder'
end
+
+ it_behaves_like 'tracking unique visits', :folder do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'staging-1.0'
+ }
+ end
+
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context 'when using JSON format' do
+ before do
+ create(:environment, project: project,
+ name: 'staging-1.0/review',
+ state: :available)
+ create(:environment, project: project,
+ name: 'staging-1.0/zzz',
+ state: :available)
+ end
+
+ let(:environments) { json_response['environments'] }
+
it 'sorts the subfolders lexicographically' do
get :folder, params: {
namespace_id: project.namespace,
@@ -187,6 +224,19 @@ RSpec.describe Projects::EnvironmentsController do
expect(json_response['environments'][1])
.to include('name' => 'staging-1.0/zzz', 'name_without_type' => 'zzz')
end
+
+ it 'handles search option properly' do
+ get(:folder, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'staging-1.0',
+ search: 'staging-1.0/z'
+ }, format: :json)
+
+ expect(environments.map { |env| env['name'] } ).to eq(['staging-1.0/zzz'])
+ expect(json_response['available_count']).to eq 1
+ expect(json_response['stopped_count']).to eq 0
+ end
end
end
@@ -197,6 +247,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to be_ok
end
+
+ it_behaves_like 'tracking unique visits', :show do
+ let(:request_params) { environment_params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context 'with invalid id' do
@@ -210,12 +265,30 @@ RSpec.describe Projects::EnvironmentsController do
end
end
+ describe 'GET new' do
+ it 'responds with a status code 200' do
+ get :new, params: environment_params
+
+ expect(response).to be_ok
+ end
+
+ it_behaves_like 'tracking unique visits', :new do
+ let(:request_params) { environment_params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
+ end
+
describe 'GET edit' do
it 'responds with a status code 200' do
get :edit, params: environment_params
expect(response).to be_ok
end
+
+ it_behaves_like 'tracking unique visits', :edit do
+ let(:request_params) { environment_params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
describe 'PATCH #update' do
@@ -230,6 +303,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{environment.id}")
end
+
+ it_behaves_like 'tracking unique visits', :update do
+ let(:request_params) { params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context "when environment params are invalid" do
@@ -294,6 +372,11 @@ RSpec.describe Projects::EnvironmentsController do
{ 'redirect_url' =>
project_environment_url(project, environment) })
end
+
+ it_behaves_like 'tracking unique visits', :stop do
+ let(:request_params) { environment_params(format: :json) }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context 'when no stop action' do
@@ -321,6 +404,11 @@ RSpec.describe Projects::EnvironmentsController do
it_behaves_like 'successful response for #cancel_auto_stop'
+ it_behaves_like 'tracking unique visits', :cancel_auto_stop do
+ let(:request_params) { environment_params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
+
context 'when user is reporter' do
let(:user) { reporter }
@@ -357,6 +445,11 @@ RSpec.describe Projects::EnvironmentsController do
get :terminal, params: environment_params
end
+
+ it_behaves_like 'tracking unique visits', :terminal do
+ let(:request_params) { environment_params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context 'with invalid id' do
@@ -859,6 +952,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{json_response['environment']['id']}")
end
+
+ it_behaves_like 'tracking unique visits', :create do
+ let(:request_params) { params }
+ let(:target_id) { 'users_visiting_environments_pages' }
+ end
end
context "when environment params are invalid" do
diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb
index fd95aa44568..29ad51d590f 100644
--- a/spec/controllers/projects/feature_flags_controller_spec.rb
+++ b/spec/controllers/projects/feature_flags_controller_spec.rb
@@ -194,7 +194,7 @@ RSpec.describe Projects::FeatureFlagsController do
other_project = create(:project)
other_project.add_developer(user)
other_feature_flag = create(:operations_feature_flag, project: other_project,
- name: 'other_flag')
+ name: 'other_flag')
params = {
namespace_id: other_project.namespace,
project_id: other_project,
@@ -208,7 +208,7 @@ RSpec.describe Projects::FeatureFlagsController do
end
context 'when feature flag is not found' do
- let!(:feature_flag) { }
+ let!(:feature_flag) {}
let(:params) do
{
@@ -486,7 +486,7 @@ RSpec.describe Projects::FeatureFlagsController do
context 'when creating a version 2 feature flag with a gitlabUserList strategy' do
let!(:user_list) do
create(:operations_feature_flag_user_list, project: project,
- name: 'My List', user_xids: 'user1,user2')
+ name: 'My List', user_xids: 'user1,user2')
end
let(:params) do
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
index baee9705127..2e25b0271ce 100644
--- a/spec/controllers/projects/grafana_api_controller_spec.rb
+++ b/spec/controllers/projects/grafana_api_controller_spec.rb
@@ -52,8 +52,8 @@ RSpec.describe Projects::GrafanaApiController do
.with(project, '1', 'api/v1/query_range',
{ 'query' => params[:query],
'start' => params[:start_time],
- 'end' => params[:end_time],
- 'step' => params[:step] })
+ 'end' => params[:end_time],
+ 'step' => params[:step] })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({})
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index be89fa0d361..9227c7dd70a 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -89,6 +89,21 @@ RSpec.describe Projects::GraphsController do
let(:request_params) { { namespace_id: project.namespace.path, project_id: project.path, id: 'master' } }
let(:target_id) { 'p_analytics_repo' }
end
+
+ it_behaves_like 'Snowplow event tracking' do
+ subject do
+ sign_in(user)
+ get :charts, params: request_params, format: :html
+ end
+
+ let(:request_params) { { namespace_id: project.namespace.path, project_id: project.path, id: 'master' } }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'perform_analytics_usage_action' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly' }
+ let(:property) { 'p_analytics_repo' }
+ end
end
context 'when languages were previously detected' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index e4e3151dd12..556dd23c135 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -5,9 +5,25 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
include ApiHelpers
include HttpIOHelpers
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:owner) { create(:owner) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+
+ before_all do
+ project.add_owner(owner)
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+ end
+
+ let(:user) { developer }
+
let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:user) { create(:user) }
before do
stub_feature_flags(ci_enable_live_trace: true)
@@ -136,9 +152,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when requesting JSON' do
let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:user) { developer }
before do
- project.add_developer(user)
sign_in(user)
allow_any_instance_of(Ci::Build)
@@ -307,9 +323,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:environment) { create(:environment, project: project, name: 'staging', state: :available) }
let(:job) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) }
+ let(:user) { maintainer }
+
before do
create(:deployment, :success, :on_cluster, environment: environment, project: project)
- project.add_maintainer(user) # Need to be a maintianer to view cluster.path
end
it 'exposes the deployment information' do
@@ -330,9 +347,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'that belongs to the project' do
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:job) { create(:ci_build, :success, pipeline: pipeline, runner: runner) }
+ let(:user) { maintainer }
before do
- project.add_maintainer(user)
sign_in(user)
end
@@ -349,10 +366,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:group) { create(:group) }
let(:runner) { create(:ci_runner, :group, groups: [group]) }
let(:job) { create(:ci_build, :success, pipeline: pipeline, runner: runner) }
- let(:user) { create(:user, :admin) }
+ let(:user) { maintainer }
before do
- project.add_maintainer(user)
sign_in(user)
end
@@ -368,10 +384,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'that belongs to instance' do
let(:runner) { create(:ci_runner, :instance) }
let(:job) { create(:ci_build, :success, pipeline: pipeline, runner: runner) }
- let(:user) { create(:user, :admin) }
+ let(:user) { maintainer }
before do
- project.add_maintainer(user)
sign_in(user)
end
@@ -421,6 +436,8 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when user is developer' do
+ let(:user) { developer }
+
it 'settings_path is not available' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
@@ -429,10 +446,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when user is maintainer' do
- let(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
- project.add_maintainer(user)
sign_in(user)
end
@@ -499,9 +515,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:trigger) { create(:ci_trigger, project: project) }
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
+ let(:user) { developer }
before do
- project.add_developer(user)
sign_in(user)
allow_any_instance_of(Ci::Build)
@@ -526,9 +542,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'user is a maintainer' do
- before do
- project.add_maintainer(user)
+ let(:user) { maintainer }
+ before do
get_show_json
end
@@ -579,7 +595,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
def get_show_json
expect { get_show(id: job.id, format: :json) }
- .to change { Gitlab::GitalyClient.get_request_count }.by_at_most(2)
+ .to change { Gitlab::GitalyClient.get_request_count }.by_at_most(3)
end
def get_show(**extra_params)
@@ -617,8 +633,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let!(:variable) { create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true') }
context 'with proper permissions on a project' do
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
end
@@ -630,8 +647,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'without proper permissions for debug logging' do
+ let(:user) { guest }
+
before do
- project.add_guest(user)
sign_in(user)
end
@@ -700,7 +718,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to match_response_schema('job/build_trace')
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
- expect(json_response['lines'].flat_map {|l| l['content'].map { |c| c['text'] } }).to include("ヾ(´༎ຶД༎ຶ`)ノ")
+ expect(json_response['lines'].flat_map { |l| l['content'].map { |c| c['text'] } }).to include("ヾ(´༎ຶД༎ຶ`)ノ")
end
end
@@ -753,8 +771,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'POST retry' do
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
end
@@ -817,12 +836,13 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'POST play' do
let(:variable_attributes) { [] }
+ let(:user) { developer }
before do
project.add_developer(user)
create(:protected_branch, :developers_can_merge,
- name: 'master', project: project)
+ name: 'protected-branch', project: project)
sign_in(user)
end
@@ -899,8 +919,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'POST cancel' do
context 'when user is authorized to cancel the build' do
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
end
@@ -965,8 +986,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when user is not authorized to cancel the build' do
let!(:job) { create(:ci_build, :cancelable, pipeline: pipeline) }
+ let(:user) { guest }
+
before do
- project.add_reporter(user)
sign_in(user)
post_cancel
@@ -990,12 +1012,13 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'POST unschedule' do
before do
- create(:protected_branch, :developers_can_merge, name: 'master', project: project)
+ create(:protected_branch, :developers_can_merge, name: 'protected-branch', project: project)
end
context 'when user is authorized to unschedule the build' do
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
post_unschedule
@@ -1025,9 +1048,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when user is not authorized to unschedule the build' do
let(:job) { create(:ci_build, :scheduled, pipeline: pipeline) }
+ let(:user) { guest }
before do
- project.add_reporter(user)
sign_in(user)
post_unschedule
@@ -1048,10 +1071,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'POST erase' do
- let(:role) { :maintainer }
+ let(:user) { maintainer }
before do
- project.add_role(user, role)
sign_in(user)
end
@@ -1097,7 +1119,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when user is developer' do
- let(:role) { :developer }
+ let(:user) { developer }
let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline, user: triggered_by) }
context 'when triggered by same user' do
@@ -1109,7 +1131,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when triggered by different user' do
- let(:triggered_by) { create(:user) }
+ let(:triggered_by) { maintainer }
it 'does not have successful status' do
expect(response).not_to have_gitlab_http_status(:found)
@@ -1168,8 +1190,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'with proper permissions for debug logging on a project' do
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
end
@@ -1181,8 +1204,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'without proper permissions for debug logging on a project' do
+ let(:user) { reporter }
+
before do
- project.add_reporter(user)
sign_in(user)
end
@@ -1218,37 +1242,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context "when job has a trace in database" do
- let(:job) { create(:ci_build, pipeline: pipeline) }
-
- before do
- job.update_column(:trace, "Sample trace")
- end
-
- it 'sends a trace file' do
- response = subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(response.headers['Content-Disposition']).to match(/^inline/)
- expect(response.body).to eq('Sample trace')
- end
-
- context 'when trace format is not text/plain' do
- before do
- job.update_column(:trace, '<html></html>')
- end
-
- it 'sets content disposition to attachment' do
- response = subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(response.headers['Content-Disposition']).to match(/^attachment/)
- end
- end
- end
-
context 'when job does not have a trace file' do
let(:job) { create(:ci_build, pipeline: pipeline) }
@@ -1274,8 +1267,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'GET #terminal' do
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
end
@@ -1323,8 +1317,9 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET #terminal_websocket_authorize' do
let!(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline, user: user) }
+ let(:user) { developer }
+
before do
- project.add_developer(user)
sign_in(user)
end
@@ -1375,14 +1370,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'GET #proxy_websocket_authorize' do
- let_it_be(:owner) { create(:owner) }
- let_it_be(:admin) { create(:admin) }
- let_it_be(:maintainer) { create(:user) }
- let_it_be(:developer) { create(:user) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:project) { create(:project, :private, :repository, namespace: owner.namespace) }
-
let(:user) { maintainer }
let(:pipeline) { create(:ci_pipeline, project: project, source: :webide, config_source: :webide_source, user: user) }
let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline, user: user) }
@@ -1407,11 +1394,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
stub_feature_flags(build_service_proxy: true)
allow(job).to receive(:has_terminal?).and_return(true)
- project.add_maintainer(maintainer)
- project.add_developer(developer)
- project.add_reporter(reporter)
- project.add_guest(guest)
-
sign_in(user)
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 776ed9774b1..a5259522fe2 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -25,10 +25,10 @@ RSpec.describe Projects::LabelsController do
let_it_be(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') }
let_it_be(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') }
- let_it_be(:group_labels) { [group_label_3, group_label_4]}
- let_it_be(:project_labels) { [label_4, label_5]}
- let_it_be(:group_priority_labels) { [group_label_1, group_label_2]}
- let_it_be(:project_priority_labels) { [label_1, label_2, label_3]}
+ let_it_be(:group_labels) { [group_label_3, group_label_4] }
+ let_it_be(:project_labels) { [label_4, label_5] }
+ let_it_be(:group_priority_labels) { [group_label_1, group_label_2] }
+ let_it_be(:project_priority_labels) { [label_1, label_2, label_3] }
before do
create(:label_priority, project: project, label: group_label_1, priority: 3)
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index b9ede84157d..182d654aaa8 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Projects::MergeRequests::DraftsController do
include RepoHelpers
let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: create(:user)) }
let(:user) { project.first_owner }
let(:user2) { create(:user) }
@@ -404,6 +404,11 @@ RSpec.describe Projects::MergeRequests::DraftsController do
end
context 'when feature flag is enabled' do
+ before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_submit_review_comment)
+ end
+
it 'creates note' do
post :publish, params: params.merge!(note: 'Hello world')
@@ -415,6 +420,72 @@ RSpec.describe Projects::MergeRequests::DraftsController do
expect(merge_request.notes.reload.size).to be(1)
end
+
+ it 'tracks merge request activity' do
+ post :publish, params: params.merge!(note: 'Hello world')
+
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_submit_review_comment).with(user: user)
+ end
+ end
+ end
+
+ context 'approve merge request' do
+ before do
+ create(:draft_note, merge_request: merge_request, author: user)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(mr_review_submit_comment: false)
+ end
+
+ it 'does not approve' do
+ post :publish, params: params.merge!(approve: true)
+
+ expect(merge_request.approvals.reload.size).to be(0)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_submit_review_approve)
+ end
+
+ it 'approves merge request' do
+ post :publish, params: params.merge!(approve: true)
+
+ expect(merge_request.approvals.reload.size).to be(1)
+ end
+
+ it 'does not approve merge request' do
+ post :publish, params: params.merge!(approve: false)
+
+ expect(merge_request.approvals.reload.size).to be(0)
+ end
+
+ it 'tracks merge request activity' do
+ post :publish, params: params.merge!(approve: true)
+
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_submit_review_approve).with(user: user)
+ end
+
+ context 'when merge request is already approved by user' do
+ before do
+ create(:approval, merge_request: merge_request, user: user)
+ end
+
+ it 'does return 200' do
+ post :publish, params: params.merge!(approve: true)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_submit_review_approve).with(user: user)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index ed5e32df8ea..9c4baeae836 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -896,12 +896,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject do
- get :exposed_artifacts, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :exposed_artifacts,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
describe 'permissions on a public project with private CI/CD' do
@@ -1031,12 +1032,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject do
- get :coverage_reports, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :coverage_reports,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
describe 'permissions on a public project with private CI/CD' do
@@ -1161,12 +1163,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject(:get_codequality_mr_diff_reports) do
- get :codequality_mr_diff_reports, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :codequality_mr_diff_reports,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
context 'permissions on a public project with private CI/CD' do
@@ -1264,12 +1267,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject do
- get :terraform_reports, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :terraform_reports,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
describe 'permissions on a public project with private CI/CD' do
@@ -1394,12 +1398,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject do
- get :test_reports, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :test_reports,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
before do
@@ -1522,12 +1527,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject do
- get :accessibility_reports, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :accessibility_reports,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
context 'permissions on a public project with private CI/CD' do
@@ -1642,12 +1648,13 @@ RSpec.describe Projects::MergeRequestsController do
end
subject do
- get :codequality_reports, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :codequality_reports,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
end
context 'permissions on a public project with private CI/CD' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 9050765afd6..1f8e96258ca 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -659,7 +659,7 @@ RSpec.describe Projects::NotesController do
context 'when target_id and noteable_id do not match' do
let(:locked_issue) { create(:issue, :locked, project: project) }
- let(:issue) {create(:issue, project: project)}
+ let(:issue) { create(:issue, project: project) }
it 'uses target_id and ignores noteable_id' do
request_params = {
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 77acd5fe13c..5bcfae4227c 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -354,7 +354,8 @@ RSpec.describe Projects::PipelineSchedulesController do
end
def go
- put :update, params: {
+ put :update,
+ params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: pipeline_schedule,
diff --git a/spec/controllers/projects/pipelines/tests_controller_spec.rb b/spec/controllers/projects/pipelines/tests_controller_spec.rb
index ddcab8b048e..07e3c28f685 100644
--- a/spec/controllers/projects/pipelines/tests_controller_spec.rb
+++ b/spec/controllers/projects/pipelines/tests_controller_spec.rb
@@ -86,11 +86,12 @@ RSpec.describe Projects::Pipelines::TestsController do
# Each test failure in this pipeline has a matching failure in the default branch
recent_failures = json_response['test_cases'].map { |tc| tc['recent_failures'] }
- expect(recent_failures).to eq([
- { 'count' => 1, 'base_branch' => 'master' },
- { 'count' => 1, 'base_branch' => 'master' },
- { 'count' => 1, 'base_branch' => 'master' }
- ])
+ expect(recent_failures).to eq(
+ [
+ { 'count' => 1, 'base_branch' => 'master' },
+ { 'count' => 1, 'base_branch' => 'master' },
+ { 'count' => 1, 'base_branch' => 'master' }
+ ])
end
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 06930d8727b..b9acaf65892 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -270,9 +270,13 @@ RSpec.describe Projects::PipelinesController do
user: user,
merge_request: merge_request)
- create_build(pipeline, 'build', 1, 'build', user)
- create_build(pipeline, 'test', 2, 'test', user)
- create_build(pipeline, 'deploy', 3, 'deploy', user)
+ build_stage = create(:ci_stage, name: 'build', pipeline: pipeline)
+ test_stage = create(:ci_stage, name: 'test', pipeline: pipeline)
+ deploy_stage = create(:ci_stage, name: 'deploy', pipeline: pipeline)
+
+ create_build(pipeline, build_stage, 1, 'build', user)
+ create_build(pipeline, test_stage, 2, 'test', user)
+ create_build(pipeline, deploy_stage, 3, 'deploy', user)
pipeline
end
@@ -284,7 +288,7 @@ RSpec.describe Projects::PipelinesController do
:artifacts,
artifacts_expire_at: 2.days.from_now,
pipeline: pipeline,
- stage: stage,
+ ci_stage: stage,
stage_idx: stage_idx,
name: name,
status: status,
@@ -327,21 +331,24 @@ RSpec.describe Projects::PipelinesController do
render_views
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+ let_it_be(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
+ let_it_be(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
def create_build_with_artifacts(stage, stage_idx, name, status)
- create(:ci_build, :artifacts, :tags, status, user: user, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+ create(:ci_build, :artifacts, :tags, status, user: user, pipeline: pipeline, ci_stage: stage, stage_idx: stage_idx, name: name)
end
def create_bridge(stage, stage_idx, name, status)
- create(:ci_bridge, status, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+ create(:ci_bridge, status, pipeline: pipeline, ci_stage: stage, stage_idx: stage_idx, name: name)
end
before do
- create_build_with_artifacts('build', 0, 'job1', :failed)
- create_build_with_artifacts('build', 0, 'job2', :running)
- create_build_with_artifacts('build', 0, 'job3', :pending)
- create_bridge('deploy', 1, 'deploy-a', :failed)
- create_bridge('deploy', 1, 'deploy-b', :created)
+ create_build_with_artifacts(build_stage, 0, 'job1', :failed)
+ create_build_with_artifacts(build_stage, 0, 'job2', :running)
+ create_build_with_artifacts(build_stage, 0, 'job3', :pending)
+ create_bridge(deploy_stage, 1, 'deploy-a', :failed)
+ create_bridge(deploy_stage, 1, 'deploy-b', :created)
end
it 'avoids N+1 database queries', :request_store, :use_sql_query_cache do
@@ -354,11 +361,11 @@ RSpec.describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(:ok)
end
- create_build_with_artifacts('build', 0, 'job4', :failed)
- create_build_with_artifacts('build', 0, 'job5', :running)
- create_build_with_artifacts('build', 0, 'job6', :pending)
- create_bridge('deploy', 1, 'deploy-c', :failed)
- create_bridge('deploy', 1, 'deploy-d', :created)
+ create_build_with_artifacts(build_stage, 0, 'job4', :failed)
+ create_build_with_artifacts(build_stage, 0, 'job5', :running)
+ create_build_with_artifacts(build_stage, 0, 'job6', :pending)
+ create_bridge(deploy_stage, 1, 'deploy-c', :failed)
+ create_bridge(deploy_stage, 1, 'deploy-d', :created)
expect do
get_pipeline_html
@@ -402,11 +409,16 @@ RSpec.describe Projects::PipelinesController do
sha: project.commit.id)
end
+ let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+ let(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
+ let(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
+ let(:post_deploy_stage) { create(:ci_stage, name: 'post deploy', pipeline: pipeline) }
+
before do
- create_build('build', 0, 'build')
- create_build('test', 1, 'rspec 0')
- create_build('deploy', 2, 'production')
- create_build('post deploy', 3, 'pages 0')
+ create_build(build_stage, 0, 'build')
+ create_build(test_stage, 1, 'rspec 0')
+ create_build(deploy_stage, 2, 'production')
+ create_build(post_deploy_stage, 3, 'pages 0')
end
it 'does not perform N + 1 queries' do
@@ -612,7 +624,9 @@ RSpec.describe Projects::PipelinesController do
def create_pipeline(project)
create(:ci_empty_pipeline, project: project).tap do |pipeline|
- create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec')
+ create(:ci_build, pipeline: pipeline,
+ ci_stage: create(:ci_stage, name: 'test', pipeline: pipeline),
+ name: 'rspec')
end
end
@@ -642,7 +656,7 @@ RSpec.describe Projects::PipelinesController do
end
def create_build(stage, stage_idx, name)
- create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+ create(:ci_build, pipeline: pipeline, ci_stage: stage, stage_idx: stage_idx, name: name)
end
end
@@ -654,10 +668,12 @@ RSpec.describe Projects::PipelinesController do
describe 'GET dag.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+ let(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
before do
- create_build('build', 1, 'build')
- create_build('test', 2, 'test', scheduling_type: 'dag').tap do |job|
+ create_build(build_stage, 1, 'build')
+ create_build(test_stage, 2, 'test', scheduling_type: 'dag').tap do |job|
create(:ci_build_need, build: job, name: 'build')
end
end
@@ -681,7 +697,7 @@ RSpec.describe Projects::PipelinesController do
end
def create_build(stage, stage_idx, name, params = {})
- create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, **params)
+ create(:ci_build, pipeline: pipeline, ci_stage: stage, stage_idx: stage_idx, name: name, **params)
end
end
@@ -730,11 +746,12 @@ RSpec.describe Projects::PipelinesController do
describe 'GET stages.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
context 'when accessing existing stage' do
before do
- create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build')
- create(:ci_build, pipeline: pipeline, stage: 'build')
+ create(:ci_build, :retried, :failed, pipeline: pipeline, ci_stage: build_stage)
+ create(:ci_build, pipeline: pipeline, ci_stage: build_stage)
end
context 'without retried' do
@@ -841,6 +858,18 @@ RSpec.describe Projects::PipelinesController do
let(:request_params) { { namespace_id: project.namespace, project_id: project, id: pipeline.id, chart: tab[:chart_param] } }
let(:target_id) { ['p_analytics_pipelines', tab[:event]] }
end
+
+ it_behaves_like 'Snowplow event tracking' do
+ subject { get :charts, params: request_params, format: :html }
+
+ let(:request_params) { { namespace_id: project.namespace, project_id: project, id: pipeline.id, chart: tab[:chart_param] } }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'perform_analytics_usage_action' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly' }
+ let(:property) { 'p_analytics_pipelines' }
+ end
end
end
@@ -965,8 +994,8 @@ RSpec.describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['errors']).to eq([
- 'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'
- ])
+ 'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'
+ ])
expect(json_response['warnings'][0]).to include(
'jobs:build may allow multiple pipelines to run for a single action due to `rules:when`'
)
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 46eb340cbba..fb27fe58cd9 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -5,6 +5,7 @@ require('spec_helper')
RSpec.describe Projects::ProjectMembersController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
+ let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:project, reload: true) { create(:project, :public) }
before do
@@ -52,7 +53,36 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- context 'when invited members are present' do
+ context 'when project belongs to a sub-group' do
+ let_it_be(:user_in_group) { create(:user) }
+ let_it_be(:project_in_group) { create(:project, :public, group: sub_group) }
+
+ before do
+ group.add_owner(user_in_group)
+ project_in_group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'lists inherited project members by default' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
+
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
+ end
+
+ it 'lists direct project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
+
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
+ end
+
+ it 'lists inherited project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
+
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ end
+ end
+
+ context 'when invited project members are present' do
let!(:invited_member) { create(:project_member, :invited, project: project) }
before do
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index c03a280d2cd..7b786f4a8af 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -167,7 +167,7 @@ RSpec.describe Projects::Registry::TagsController do
repository_id: repository,
ids: names
},
- format: :json
+ format: :json
end
end
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index ad6682601f3..b307bb357fa 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -312,7 +312,7 @@ RSpec.describe Projects::ReleasesController do
end
context 'suffix path abuse' do
- let(:suffix_path) { 'downloads/zips/../../../../../../../robots.txt'}
+ let(:suffix_path) { 'downloads/zips/../../../../../../../robots.txt' }
it 'raises attack error' do
expect do
diff --git a/spec/controllers/projects/service_desk_controller_spec.rb b/spec/controllers/projects/service_desk_controller_spec.rb
index 1c4d6665414..e078bf9461e 100644
--- a/spec/controllers/projects/service_desk_controller_spec.rb
+++ b/spec/controllers/projects/service_desk_controller_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Projects::ServiceDeskController do
let_it_be(:project) do
- create(:project, :private, :custom_repo, service_desk_enabled: true,
- files: { '.gitlab/issue_templates/service_desk.md' => 'template' })
+ create(:project, :private, :custom_repo,
+ service_desk_enabled: true,
+ files: { '.gitlab/issue_templates/service_desk.md' => 'template' })
end
let_it_be(:user) { create(:user) }
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index 8ee9f22aa7f..b76269f6f93 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Projects::Settings::IntegrationsController do
end
end
- context 'when validations fail' do
+ context 'when validations fail', :clean_gitlab_redis_rate_limiting do
let(:integration_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do
@@ -62,7 +62,7 @@ RSpec.describe Projects::Settings::IntegrationsController do
end
end
- context 'when successful' do
+ context 'when successful', :clean_gitlab_redis_rate_limiting do
context 'with empty project' do
let_it_be(:project) { create(:project) }
@@ -200,8 +200,8 @@ RSpec.describe Projects::Settings::IntegrationsController do
2.times { post :test, params: project_params(service: integration_params) }
- expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
- expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.body).to include(_('This endpoint has been requested too many times. Try again later.'))
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
diff --git a/spec/controllers/projects/settings/merge_requests_controller_spec.rb b/spec/controllers/projects/settings/merge_requests_controller_spec.rb
new file mode 100644
index 00000000000..106ec62bea0
--- /dev/null
+++ b/spec/controllers/projects/settings/merge_requests_controller_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Settings::MergeRequestsController do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+
+ describe '#update', :enable_admin_mode do
+ render_views
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'updates Fast Forward Merge attributes' do
+ controller.instance_variable_set(:@project, project)
+
+ params = {
+ merge_method: :ff
+ }
+
+ put :update,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project.id,
+ project: params
+ }
+
+ expect(response).to redirect_to project_settings_merge_requests_path(project)
+ params.each do |param, value|
+ expect(project.public_send(param)).to eq(value)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 143516e4712..9bc3065b6da 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -163,8 +163,8 @@ RSpec.describe Projects::TreeController do
end
context 'successful creation' do
- let(:path) { 'files/new_dir'}
- let(:branch_name) { 'master-test'}
+ let(:path) { 'files/new_dir' }
+ let(:branch_name) { 'master-test' }
it 'redirects to the new directory' do
expect(subject)
@@ -175,7 +175,7 @@ RSpec.describe Projects::TreeController do
context 'unsuccessful creation' do
let(:path) { 'README.md' }
- let(:branch_name) { 'master'}
+ let(:branch_name) { 'master' }
it 'does not allow overwriting of existing files' do
expect(subject)
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 94d75ab8d7d..b30610d98d7 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -920,6 +920,7 @@ RSpec.describe ProjectsController do
environments_access_level
feature_flags_access_level
releases_access_level
+ monitor_access_level
]
end
@@ -947,6 +948,7 @@ RSpec.describe ProjectsController do
where(:feature_access_level) do
%i[
environments_access_level feature_flags_access_level
+ monitor_access_level
]
end
@@ -1217,6 +1219,40 @@ RSpec.describe ProjectsController do
expect(json_response["Commits"]).to include("123456")
end
+ it 'uses gitaly pagination' do
+ expected_params = ActionController::Parameters.new(ref: '123456', per_page: 100).permit!
+
+ expect_next_instance_of(BranchesFinder, project.repository, expected_params) do |finder|
+ expect(finder).to receive(:execute).with(gitaly_pagination: true).and_call_original
+ end
+
+ expect_next_instance_of(TagsFinder, project.repository, expected_params) do |finder|
+ expect(finder).to receive(:execute).with(gitaly_pagination: true).and_call_original
+ end
+
+ get :refs, params: { namespace_id: project.namespace, id: project, ref: "123456" }
+ end
+
+ context 'when use_gitaly_pagination_for_refs is disabled' do
+ before do
+ stub_feature_flags(use_gitaly_pagination_for_refs: false)
+ end
+
+ it 'does not use gitaly pagination' do
+ expected_params = ActionController::Parameters.new(ref: '123456', per_page: 100).permit!
+
+ expect_next_instance_of(BranchesFinder, project.repository, expected_params) do |finder|
+ expect(finder).to receive(:execute).with(gitaly_pagination: false).and_call_original
+ end
+
+ expect_next_instance_of(TagsFinder, project.repository, expected_params) do |finder|
+ expect(finder).to receive(:execute).with(gitaly_pagination: false).and_call_original
+ end
+
+ get :refs, params: { namespace_id: project.namespace, id: project, ref: "123456" }
+ end
+ end
+
context 'when gitaly is unavailable' do
before do
expect_next_instance_of(TagsFinder) do |finder|
diff --git a/spec/controllers/registrations/welcome_controller_spec.rb b/spec/controllers/registrations/welcome_controller_spec.rb
index 8a5a8490a23..14e88d469ba 100644
--- a/spec/controllers/registrations/welcome_controller_spec.rb
+++ b/spec/controllers/registrations/welcome_controller_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Registrations::WelcomeController do
sign_in(user)
end
- it { is_expected.to redirect_to(dashboard_projects_path)}
+ it { is_expected.to redirect_to(dashboard_projects_path) }
end
context 'when role is set and setup_for_company is not set' do
@@ -78,7 +78,7 @@ RSpec.describe Registrations::WelcomeController do
sign_in(user)
end
- it { is_expected.to redirect_to(dashboard_projects_path)}
+ it { is_expected.to redirect_to(dashboard_projects_path) }
context 'when the new user already has any accepted group membership' do
let!(:member1) { create(:group_member, user: user) }
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 70d4559edc1..637c774c38b 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe RegistrationsController do
before do
stub_application_setting(require_admin_approval_after_user_signup: false)
+ stub_feature_flags(arkose_labs_signup_challenge: false)
end
describe '#new' do
@@ -121,6 +122,7 @@ RSpec.describe RegistrationsController do
context 'when `send_user_confirmation_email` is true' do
before do
stub_application_setting(send_user_confirmation_email: true)
+ stub_feature_flags(identity_verification: false)
end
it 'sends a confirmation email' do
@@ -133,6 +135,10 @@ RSpec.describe RegistrationsController do
end
context 'email confirmation' do
+ before do
+ stub_feature_flags(identity_verification: false)
+ end
+
context 'when send_user_confirmation_email is false' do
it 'signs the user in' do
stub_application_setting(send_user_confirmation_email: false)
@@ -492,6 +498,33 @@ RSpec.describe RegistrationsController do
end
end
end
+
+ context 'when the password is weak' do
+ render_views
+ let_it_be(:new_user_params) { { new_user: base_user_params.merge({ password: "password" }) } }
+
+ subject { post(:create, params: new_user_params) }
+
+ context 'when block_weak_passwords is enabled (default)' do
+ it 'renders the form with errors' do
+ expect { subject }.not_to change(User, :count)
+
+ expect(controller.current_user).to be_nil
+ expect(response).to render_template(:new)
+ expect(response.body).to include(_('Password must not contain commonly used combinations of words and letters'))
+ end
+ end
+
+ context 'when block_weak_passwords is disabled' do
+ before do
+ stub_feature_flags(block_weak_passwords: false)
+ end
+
+ it 'permits weak passwords' do
+ expect { subject }.to change(User, :count).by(1)
+ end
+ end
+ end
end
describe '#destroy' do
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 448587c937a..da62acb1fda 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -55,11 +55,11 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:namespace) { project.namespace }
before do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
send_request
end
- subject { OnboardingProgress.completed?(namespace, :git_pull) }
+ subject { Onboarding::Progress.completed?(namespace, :git_pull) }
it { is_expected.to be(true) }
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 14b198dbefe..4131bd148da 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -270,6 +270,59 @@ RSpec.describe SearchController do
get(:show, params: { search: 'foo@bar.com', scope: 'users' })
end
end
+
+ it 'increments the custom search sli apdex' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with(
+ elapsed: a_kind_of(Numeric),
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get :show, params: { scope: 'issues', search: 'hello world' }
+ end
+
+ context 'custom search sli error rate' do
+ context 'when the search is successful' do
+ it 'increments the custom search sli error rate with error: false' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
+ error: false,
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get :show, params: { scope: 'issues', search: 'hello world' }
+ end
+ end
+
+ context 'when the search raises an error' do
+ before do
+ allow_next_instance_of(SearchService) do |service|
+ allow(service).to receive(:search_results).and_raise(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ it 'increments the custom search sli error rate with error: true' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
+ error: true,
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get :show, params: { scope: 'issues', search: 'hello world' }
+ end
+ end
+
+ context 'when something goes wrong before a search is done' do
+ it 'does not increment the error rate' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).not_to receive(:record_error_rate)
+
+ get :show, params: { scope: 'issues' } # no search query
+ end
+ end
+ end
end
describe 'GET #count', :aggregate_failures do
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 8e85e283b31..00d99b46d0b 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -312,7 +312,7 @@ RSpec.describe Snippets::NotesController do
describe 'POST toggle_award_emoji' do
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
- let(:emoji_name) { 'thumbsup'}
+ let(:emoji_name) { 'thumbsup' }
before do
sign_in(user)
diff --git a/spec/db/development/add_security_training_providers_spec.rb b/spec/db/development/add_security_training_providers_spec.rb
new file mode 100644
index 00000000000..276fa690898
--- /dev/null
+++ b/spec/db/development/add_security_training_providers_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create security training providers in development' do
+ subject { load Rails.root.join('db', 'fixtures', 'development', '044_add_security_training_providers.rb') }
+
+ it_behaves_like 'security training providers importer'
+end
diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb
index 7987c78b423..7751bfd989d 100644
--- a/spec/db/migration_spec.rb
+++ b/spec/db/migration_spec.rb
@@ -8,10 +8,10 @@ RSpec.describe 'Migrations Validation' do
# The range describes the timestamps that given migration helper can be used
let(:all_migration_classes) do
{
- 2022_01_26_21_06_58.. => Gitlab::Database::Migration[2.0],
+ 2022_01_26_21_06_58.. => Gitlab::Database::Migration[2.0],
2021_09_01_15_33_24..2022_04_25_12_06_03 => Gitlab::Database::Migration[1.0],
2021_05_31_05_39_16..2021_09_01_15_33_24 => ActiveRecord::Migration[6.1],
- ..2021_05_31_05_39_16 => ActiveRecord::Migration[6.0]
+ ..2021_05_31_05_39_16 => ActiveRecord::Migration[6.0]
}
end
diff --git a/spec/db/production/add_security_training_providers_spec.rb b/spec/db/production/add_security_training_providers_spec.rb
new file mode 100644
index 00000000000..50d0653e7a2
--- /dev/null
+++ b/spec/db/production/add_security_training_providers_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create security training providers in production' do
+ subject { load Rails.root.join('db', 'fixtures', 'production', '004_add_security_training_providers.rb') }
+
+ it_behaves_like 'security training providers importer'
+end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index bd13f86034a..4aeafed5712 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -31,9 +31,14 @@ RSpec.describe 'Database schema' do
boards: %w[milestone_id iteration_id],
chat_names: %w[chat_id team_id user_id],
chat_teams: %w[team_id],
- ci_builds: %w[erased_by_id trigger_request_id],
+ ci_builds: %w[erased_by_id trigger_request_id partition_id],
+ ci_builds_metadata: %w[partition_id],
+ ci_job_artifacts: %w[partition_id],
ci_namespace_monthly_usages: %w[namespace_id],
+ ci_pipeline_variables: %w[partition_id],
+ ci_pipelines: %w[partition_id],
ci_runner_projects: %w[runner_id],
+ ci_stages: %w[partition_id],
ci_trigger_requests: %w[commit_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
cluster_providers_gcp: %w[gcp_project_id operation_id],
@@ -50,6 +55,7 @@ RSpec.describe 'Database schema' do
geo_node_statuses: %w[last_event_id cursor_last_event_id],
geo_nodes: %w[oauth_application_id],
geo_repository_deleted_events: %w[project_id],
+ ghost_user_migrations: %w[initiator_user_id],
gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
identities: %w[user_id],
import_failures: %w[project_id],
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
index 8956fa44b7a..470b1512a83 100644
--- a/spec/dependencies/omniauth_saml_spec.rb
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'processing of SAMLResponse in dependencies' do
allow_next_instance_of(OneLogin::RubySaml::Response) do |instance|
allow(instance).to receive(:is_valid?).and_return(true)
end
- saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) { }
+ saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) {}
end
it 'can extract AuthnContextClassRef from SAMLResponse param' do
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index fa4fdf805ec..94bd88fbc75 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -58,7 +58,7 @@ module DeprecationToolkitEnv
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
def self.allowed_kwarg_warning_paths
%w[
- ]
+ ]
end
def self.configure!
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index a39890dd530..b144e6f77d2 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe ApplicationExperiment, :experiment do
# them optional there.
expect(experiment(:example)).to register_behavior(:control).with(nil)
- expect { experiment(:example) { } }.not_to raise_error
+ expect { experiment(:example) {} }.not_to raise_error
end
describe "#publish" do
@@ -129,7 +129,7 @@ RSpec.describe ApplicationExperiment, :experiment do
expect_snowplow_event(
category: 'namespaced/stub',
- action: 'action',
+ action: :action,
property: '_property_',
context: [
{
@@ -162,7 +162,7 @@ RSpec.describe ApplicationExperiment, :experiment do
expect_snowplow_event(
category: 'namespaced/stub',
- action: 'action',
+ action: :action,
user: user,
project: project,
namespace: namespace,
@@ -177,7 +177,7 @@ RSpec.describe ApplicationExperiment, :experiment do
expect_snowplow_event(
category: 'namespaced/stub',
- action: 'action',
+ action: :action,
user: user,
project: project,
namespace: group,
@@ -193,7 +193,7 @@ RSpec.describe ApplicationExperiment, :experiment do
expect_snowplow_event(
category: 'namespaced/stub',
- action: 'action',
+ action: :action,
user: actor,
project: project,
namespace: namespace,
@@ -208,7 +208,7 @@ RSpec.describe ApplicationExperiment, :experiment do
expect_snowplow_event(
category: 'namespaced/stub',
- action: 'action',
+ action: :action,
project: project,
namespace: namespace,
context: an_instance_of(Array)
@@ -289,15 +289,15 @@ RSpec.describe ApplicationExperiment, :experiment do
end
it "doesn't raise an exception" do
- expect { experiment(:top) { |e| e.control { experiment(:nested) { } } } }.not_to raise_error
+ expect { experiment(:top) { |e| e.control { experiment(:nested) {} } } }.not_to raise_error
end
it "tracks an event", :snowplow do
- experiment(:top) { |e| e.control { experiment(:nested) { } } }
+ experiment(:top) { |e| e.control { experiment(:nested) {} } }
expect(Gitlab::Tracking).to have_received(:event).with( # rubocop:disable RSpec/ExpectGitlabTracking
'top',
- 'nested',
+ :nested,
hash_including(label: 'nested')
)
end
@@ -311,8 +311,8 @@ RSpec.describe ApplicationExperiment, :experiment do
cache.clear(key: application_experiment.name)
- application_experiment.control { }
- application_experiment.candidate { }
+ application_experiment.control {}
+ application_experiment.candidate {}
end
it "caches the variant determined by the variant resolver" do
@@ -378,7 +378,7 @@ RSpec.describe ApplicationExperiment, :experiment do
it "doesn't warn on non dev/test environments" do
allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
- expect { experiment(:example) { |e| e.use { } } }.not_to raise_error
+ expect { experiment(:example) { |e| e.use {} } }.not_to raise_error
expect(ActiveSupport::Deprecation).not_to have_received(:new).with(anything, 'Gitlab::Experiment')
end
@@ -387,7 +387,7 @@ RSpec.describe ApplicationExperiment, :experiment do
# This will eventually raise an ActiveSupport::Deprecation exception,
# it's ok to change it when that happens.
- expect { experiment(:example) { |e| e.use { } } }.not_to raise_error
+ expect { experiment(:example) { |e| e.use {} } }.not_to raise_error
expect(ActiveSupport::Deprecation).to have_received(:new).with(anything, 'Gitlab::Experiment')
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
index 115eb32111c..64a297932de 100644
--- a/spec/factories/ci/build_trace_chunks.rb
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -23,7 +23,7 @@ FactoryBot.define do
end
trait :database_with_data do
- data_store { :database}
+ data_store { :database }
transient do
initial_data { 'test data' }
@@ -55,7 +55,7 @@ FactoryBot.define do
end
trait :persisted do
- data_store { :database}
+ data_store { :database }
transient do
initial_data { 'test data' }
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index d684f79a518..8c2edc8cd9f 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -129,8 +129,8 @@ FactoryBot.define do
{
script: %w(ls),
environment: { name: 'staging',
- action: 'stop',
- url: 'http://staging.example.com/$CI_JOB_NAME' }
+ action: 'stop',
+ url: 'http://staging.example.com/$CI_JOB_NAME' }
}
end
end
@@ -141,9 +141,9 @@ FactoryBot.define do
{
script: %w(ls),
environment: { name: 'test_portal',
- action: 'start',
- url: 'http://staging.example.com/$CI_JOB_NAME',
- deployment_tier: 'testing' }
+ action: 'start',
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ deployment_tier: 'testing' }
}
end
end
@@ -155,7 +155,7 @@ FactoryBot.define do
{
script: %w(ls),
environment: { name: 'production',
- url: 'http://prd.example.com/$CI_JOB_NAME' }
+ url: 'http://prd.example.com/$CI_JOB_NAME' }
}
end
end
@@ -167,8 +167,8 @@ FactoryBot.define do
{
script: %w(ls),
environment: { name: 'review/$CI_COMMIT_REF_NAME',
- url: 'http://staging.example.com/$CI_JOB_NAME',
- on_stop: 'stop_review_app' }
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ on_stop: 'stop_review_app' }
}
end
end
@@ -181,8 +181,8 @@ FactoryBot.define do
{
script: %w(ls),
environment: { name: 'review/$CI_COMMIT_REF_NAME',
- url: 'http://staging.example.com/$CI_JOB_NAME',
- action: 'stop' }
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ action: 'stop' }
}
end
end
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 114ad3a5847..f8b964cf8e0 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -15,7 +15,7 @@ FactoryBot.define do
end
trait :remote_store do
- file_store { JobArtifactUploader::Store::REMOTE}
+ file_store { JobArtifactUploader::Store::REMOTE }
end
after :build do |artifact|
@@ -102,28 +102,6 @@ FactoryBot.define do
end
end
- trait :zip_with_single_file do
- file_type { :archive }
- file_format { :zip }
-
- after(:build) do |artifact, evaluator|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip'),
- 'application/zip')
- end
- end
-
- trait :zip_with_multiple_files do
- file_type { :archive }
- file_format { :zip }
-
- after(:build) do |artifact, evaluator|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip'),
- 'application/zip')
- end
- end
-
trait :junit do
file_type { :junit }
file_format { :gzip }
@@ -384,6 +362,15 @@ FactoryBot.define do
end
end
+ trait :common_security_report_without_top_level_scanner do
+ common_security_report
+
+ after(:build) do |artifact, _|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json'), 'application/json')
+ end
+ end
+
trait :common_security_report_with_blank_names do
file_format { :raw }
file_type { :dependency_scanning }
diff --git a/spec/factories/ci/pipeline_artifacts.rb b/spec/factories/ci/pipeline_artifacts.rb
index 85277ce6fbf..d096f149c3a 100644
--- a/spec/factories/ci/pipeline_artifacts.rb
+++ b/spec/factories/ci/pipeline_artifacts.rb
@@ -30,7 +30,7 @@ FactoryBot.define do
end
trait :remote_store do
- file_store { ::ObjectStorage::Store::REMOTE}
+ file_store { ::ObjectStorage::Store::REMOTE }
end
trait :with_coverage_report do
diff --git a/spec/factories/ci/reports/sbom/components.rb b/spec/factories/ci/reports/sbom/components.rb
new file mode 100644
index 00000000000..317e1c863cf
--- /dev/null
+++ b/spec/factories/ci/reports/sbom/components.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_reports_sbom_component, class: '::Gitlab::Ci::Reports::Sbom::Component' do
+ type { :library }
+ sequence(:name) { |n| "component-#{n}" }
+ sequence(:version) { |n| "v0.0.#{n}" }
+
+ skip_create
+
+ initialize_with do
+ ::Gitlab::Ci::Reports::Sbom::Component.new(
+ type: type,
+ name: name,
+ version: version
+ )
+ end
+ end
+end
diff --git a/spec/factories/ci/reports/sbom/sources.rb b/spec/factories/ci/reports/sbom/sources.rb
new file mode 100644
index 00000000000..9093aba86a6
--- /dev/null
+++ b/spec/factories/ci/reports/sbom/sources.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_reports_sbom_source, class: '::Gitlab::Ci::Reports::Sbom::Source' do
+ type { :dependency_scanning }
+
+ transient do
+ sequence(:input_file_path) { |n| "subproject-#{n}/package-lock.json" }
+ sequence(:source_file_path) { |n| "subproject-#{n}/package.json" }
+ end
+
+ data do
+ {
+ 'category' => 'development',
+ 'input_file' => { 'path' => input_file_path },
+ 'source_file' => { 'path' => source_file_path },
+ 'package_manager' => { 'name' => 'npm' },
+ 'language' => { 'name' => 'JavaScript' }
+ }
+ end
+
+ fingerprint { Digest::SHA256.hexdigest(data.to_json) }
+
+ skip_create
+
+ initialize_with do
+ ::Gitlab::Ci::Reports::Sbom::Source.new(
+ type: type,
+ data: data,
+ fingerprint: fingerprint
+ )
+ end
+ end
+end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index fa10b37cdbf..a60f0a3879a 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -6,10 +6,10 @@ FactoryBot.define do
stage { 'test' }
stage_idx { 0 }
status { 'success' }
- description { 'commit status'}
+ description { 'commit status' }
pipeline factory: :ci_pipeline
- started_at { 'Tue, 26 Jan 2016 08:21:42 +0100'}
- finished_at { 'Tue, 26 Jan 2016 08:23:42 +0100'}
+ started_at { 'Tue, 26 Jan 2016 08:21:42 +0100' }
+ finished_at { 'Tue, 26 Jan 2016 08:23:42 +0100' }
trait :success do
status { 'success' }
@@ -56,12 +56,7 @@ FactoryBot.define do
end
after(:build) do |build, evaluator|
- build.project = build.pipeline.project
- end
-
- factory :generic_commit_status, class: 'GenericCommitStatus' do
- name { 'generic' }
- description { 'external commit status' }
+ build.project ||= build.pipeline.project
end
end
end
diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb
index 56a1b55b969..3d95c754a96 100644
--- a/spec/factories/design_management/designs.rb
+++ b/spec/factories/design_management/designs.rb
@@ -109,7 +109,7 @@ FactoryBot.define do
repository = project.design_repository
commit_version = ->(action) do
- repository.multi_action(
+ repository.commit_files(
evaluator.author,
branch_name: 'master',
message: "#{action.action} for #{design.filename}",
diff --git a/spec/factories/design_management/versions.rb b/spec/factories/design_management/versions.rb
index e505a77d6bd..9d965c6e86c 100644
--- a/spec/factories/design_management/versions.rb
+++ b/spec/factories/design_management/versions.rb
@@ -102,7 +102,7 @@ FactoryBot.define do
end
if actions.present?
- repository.multi_action(
+ repository.commit_files(
evaluator.author,
branch_name: 'master',
message: "created #{actions.size} files",
@@ -123,7 +123,7 @@ FactoryBot.define do
end
end
- sha = repository.multi_action(
+ sha = repository.commit_files(
evaluator.author,
branch_name: 'master',
message: "edited #{version_actions.size} files",
diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb
index b30fa8a5896..e51c3358a9b 100644
--- a/spec/factories/emails.rb
+++ b/spec/factories/emails.rb
@@ -6,6 +6,6 @@ FactoryBot.define do
email { generate(:email_alias) }
trait(:confirmed) { confirmed_at { Time.now } }
- trait(:skip_validate) { to_create {|instance| instance.save!(validate: false) } }
+ trait(:skip_validate) { to_create { |instance| instance.save!(validate: false) } }
end
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index ccd2011eb8d..34843dab0fe 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -47,7 +47,7 @@ FactoryBot.define do
pipeline = create(:ci_pipeline, project: environment.project)
deployable = create(:ci_build, :success, name: "#{environment.name}:deploy",
- pipeline: pipeline)
+ pipeline: pipeline)
deployment = create(:deployment,
:success,
diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/external_pull_requests.rb
index 7a6e77f8572..470814f4360 100644
--- a/spec/factories/external_pull_requests.rb
+++ b/spec/factories/external_pull_requests.rb
@@ -12,6 +12,6 @@ FactoryBot.define do
target_sha { 'a09386439ca39abe575675ffd4b89ae824fec22f' }
status { :open }
- trait(:closed) { status { 'closed'} }
+ trait(:closed) { status { 'closed' } }
end
end
diff --git a/spec/factories/generic_commit_statuses.rb b/spec/factories/generic_commit_statuses.rb
new file mode 100644
index 00000000000..ea86dddcaf8
--- /dev/null
+++ b/spec/factories/generic_commit_statuses.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :generic_commit_status, class: 'GenericCommitStatus', parent: :commit_status do
+ name { 'generic' }
+ description { 'external commit status' }
+ end
+end
diff --git a/spec/factories/gitlab/database/postgres_index.rb b/spec/factories/gitlab/database/postgres_index.rb
index 54bbb738512..8492b44c404 100644
--- a/spec/factories/gitlab/database/postgres_index.rb
+++ b/spec/factories/gitlab/database/postgres_index.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
exclusion { false }
expression { false }
partial { false }
- definition { "CREATE INDEX #{identifier} ON #{tablename} (bar)"}
+ definition { "CREATE INDEX #{identifier} ON #{tablename} (bar)" }
ondisk_size_bytes { 100.megabytes }
end
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 4b1bf9a7d11..702db45554e 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :group_member do
access_level { GroupMember::OWNER }
source { association(:group) }
+ member_namespace_id { source.id }
user
trait(:guest) { access_level { GroupMember::GUEST } }
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 152ae061605..6f9cf0ef895 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -106,9 +106,9 @@ FactoryBot.define do
end
create_graph(
- parent: group,
+ parent: group,
children: evaluator.children,
- depth: evaluator.depth
+ depth: evaluator.depth
)
end
end
diff --git a/spec/factories/ml/candidates.rb b/spec/factories/ml/candidates.rb
new file mode 100644
index 00000000000..b5644ee3841
--- /dev/null
+++ b/spec/factories/ml/candidates.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+FactoryBot.define do
+ factory :ml_candidates, class: '::Ml::Candidate' do
+ association :experiment, factory: :ml_experiments
+ association :user
+ end
+end
diff --git a/spec/factories/ml/experiments.rb b/spec/factories/ml/experiments.rb
new file mode 100644
index 00000000000..043ca712e60
--- /dev/null
+++ b/spec/factories/ml/experiments.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+FactoryBot.define do
+ factory :ml_experiments, class: '::Ml::Experiment' do
+ sequence(:name) { |n| "experiment#{n}" }
+ association :project
+ association :user
+ end
+end
diff --git a/spec/factories/namespaces/sync_events.rb b/spec/factories/namespaces/sync_events.rb
new file mode 100644
index 00000000000..63bb16958a3
--- /dev/null
+++ b/spec/factories/namespaces/sync_events.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :sync_event, class: 'Namespaces::SyncEvent' do
+ association :namespace, factory: :namespace, strategy: :build
+ end
+end
diff --git a/spec/factories/onboarding_progresses.rb b/spec/factories/onboarding/progresses.rb
index e39bad91b19..15f58b482d3 100644
--- a/spec/factories/onboarding_progresses.rb
+++ b/spec/factories/onboarding/progresses.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :onboarding_progress do
+ factory :onboarding_progress, class: 'Onboarding::Progress' do
namespace
end
end
diff --git a/spec/factories/packages/debian/component_file.rb b/spec/factories/packages/debian/component_file.rb
index eeba64ba5d2..a2422e4a126 100644
--- a/spec/factories/packages/debian/component_file.rb
+++ b/spec/factories/packages/debian/component_file.rb
@@ -30,10 +30,18 @@ FactoryBot.define do
trait(:sources) do
file_type { :sources }
architecture { nil }
+ file_fixture { 'spec/fixtures/packages/debian/distribution/Sources' }
end
trait(:di_packages) do
file_type { :di_packages }
+ file_fixture { 'spec/fixtures/packages/debian/distribution/D-I-Packages' }
+ end
+
+ trait(:older_sha256) do
+ created_at { '2020-01-24T08:00:00Z' }
+ file_sha256 { '157a1ad2b9102038560eea56771913b312ebf25093f5ef3b9842021c639c880d' }
+ file_fixture { 'spec/fixtures/packages/debian/distribution/OtherSHA256' }
end
trait(:object_storage) do
diff --git a/spec/factories/packages/dependencies.rb b/spec/factories/packages/dependencies.rb
index a62d48c2e73..c99f11d6db3 100644
--- a/spec/factories/packages/dependencies.rb
+++ b/spec/factories/packages/dependencies.rb
@@ -2,11 +2,11 @@
FactoryBot.define do
factory :packages_dependency, class: 'Packages::Dependency' do
- sequence(:name) { |n| "@test/package-#{n}"}
+ sequence(:name) { |n| "@test/package-#{n}" }
sequence(:version_pattern) { |n| "~6.2.#{n}" }
trait(:rubygems) do
- sequence(:name) { |n| "gem-dependency-#{n}"}
+ sequence(:name) { |n| "gem-dependency-#{n}" }
end
end
end
diff --git a/spec/factories/packages/package_files.rb b/spec/factories/packages/package_files.rb
index 5eac0036b91..7d3dd274777 100644
--- a/spec/factories/packages/package_files.rb
+++ b/spec/factories/packages/package_files.rb
@@ -337,6 +337,14 @@ FactoryBot.define do
size { 3989.bytes }
end
+ trait(:rpm) do
+ package
+ file_fixture { 'spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm' }
+ file_name { 'hello-0.0.1-1.fc29.x86_64.rpm' }
+ file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
+ size { 115.kilobytes }
+ end
+
trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end
diff --git a/spec/factories/packages/package_tags.rb b/spec/factories/packages/package_tags.rb
index 3d2eea4a73b..1a40d5d600f 100644
--- a/spec/factories/packages/package_tags.rb
+++ b/spec/factories/packages/package_tags.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :packages_tag, class: 'Packages::Tag' do
package
- sequence(:name) { |n| "tag-#{n}"}
+ sequence(:name) { |n| "tag-#{n}" }
end
end
diff --git a/spec/factories/packages/packages.rb b/spec/factories/packages/packages.rb
index 90c68074a3b..8074e505243 100644
--- a/spec/factories/packages/packages.rb
+++ b/spec/factories/packages/packages.rb
@@ -24,6 +24,10 @@ FactoryBot.define do
status { :pending_destruction }
end
+ trait :last_downloaded_at do
+ last_downloaded_at { 2.days.ago }
+ end
+
factory :maven_package do
maven_metadatum
@@ -55,6 +59,12 @@ FactoryBot.define do
end
end
+ factory :rpm_package do
+ sequence(:name) { |n| "package-#{n}" }
+ sequence(:version) { |n| "v1.0.#{n}" }
+ package_type { :rpm }
+ end
+
factory :debian_package do
sequence(:name) { |n| "package-#{n}" }
sequence(:version) { |n| "1.0-#{n}" }
@@ -115,7 +125,7 @@ FactoryBot.define do
end
factory :npm_package do
- sequence(:name) { |n| "@#{project.root_namespace.path}/package-#{n}"}
+ sequence(:name) { |n| "@#{project.root_namespace.path}/package-#{n}" }
sequence(:version) { |n| "1.0.#{n}" }
package_type { :npm }
@@ -153,7 +163,7 @@ FactoryBot.define do
end
factory :nuget_package do
- sequence(:name) { |n| "NugetPackage#{n}"}
+ sequence(:name) { |n| "NugetPackage#{n}" }
sequence(:version) { |n| "1.0.#{n}" }
package_type { :nuget }
@@ -175,7 +185,7 @@ FactoryBot.define do
end
factory :pypi_package do
- sequence(:name) { |n| "pypi-package-#{n}"}
+ sequence(:name) { |n| "pypi-package-#{n}" }
sequence(:version) { |n| "1.0.#{n}" }
package_type { :pypi }
@@ -193,7 +203,7 @@ FactoryBot.define do
end
factory :composer_package do
- sequence(:name) { |n| "composer-package-#{n}"}
+ sequence(:name) { |n| "composer-package-#{n}" }
sequence(:version) { |n| "1.0.#{n}" }
package_type { :composer }
@@ -210,7 +220,7 @@ FactoryBot.define do
end
factory :golang_package do
- sequence(:name) { |n| "golang.org/x/pkg-#{n}"}
+ sequence(:name) { |n| "golang.org/x/pkg-#{n}" }
sequence(:version) { |n| "v1.0.#{n}" }
package_type { :golang }
end
diff --git a/spec/factories/packages/rpm/metadata.rb b/spec/factories/packages/rpm/metadata.rb
new file mode 100644
index 00000000000..5ee85aed3bb
--- /dev/null
+++ b/spec/factories/packages/rpm/metadata.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :rpm_metadatum, class: 'Packages::Rpm::Metadatum' do
+ package { association(:rpm_package) }
+ release { "#{rand(10)}.#{rand(10)}" }
+ summary { FFaker::Lorem.sentences(2).join }
+ description { FFaker::Lorem.sentences(4).join }
+ arch { FFaker::Lorem.word }
+ epoch { 0 }
+ end
+end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index ab1e45acc15..57f228650a1 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :project_member do
user
source { association(:project) }
+ member_namespace_id { source.id }
maintainer
trait(:guest) { access_level { ProjectMember::GUEST } }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 95b72648cf5..871917a725e 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -35,6 +35,7 @@ FactoryBot.define do
end
metrics_dashboard_access_level { ProjectFeature::PRIVATE }
operations_access_level { ProjectFeature::ENABLED }
+ monitor_access_level { ProjectFeature::ENABLED }
container_registry_access_level { ProjectFeature::ENABLED }
security_and_compliance_access_level { ProjectFeature::PRIVATE }
environments_access_level { ProjectFeature::ENABLED }
diff --git a/spec/factories/prometheus_alert.rb b/spec/factories/prometheus_alert.rb
index ad3868c38ed..14fdd993c7a 100644
--- a/spec/factories/prometheus_alert.rb
+++ b/spec/factories/prometheus_alert.rb
@@ -15,7 +15,7 @@ FactoryBot.define do
end
trait :with_runbook_url do
- runbook_url { 'https://runbooks.gitlab.com/metric_gt_1'}
+ runbook_url { 'https://runbooks.gitlab.com/metric_gt_1' }
end
end
end
diff --git a/spec/factories/prometheus_metrics.rb b/spec/factories/prometheus_metrics.rb
index 503d392a524..e785bf2ac9d 100644
--- a/spec/factories/prometheus_metrics.rb
+++ b/spec/factories/prometheus_metrics.rb
@@ -9,7 +9,7 @@ FactoryBot.define do
group { :business }
project
legend { 'legend' }
- dashboard_path { '.gitlab/dashboards/dashboard_path.yml'}
+ dashboard_path { '.gitlab/dashboards/dashboard_path.yml' }
trait :common do
common { true }
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index bac1cf21596..425352783dd 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -6,11 +6,25 @@ FactoryBot.define do
project
transient do
+ ee { false }
default_push_level { true }
default_merge_level { true }
default_access_level { true }
end
+ after(:create) do |protected_branch, evaluator|
+ break unless protected_branch.project&.persisted?
+
+ ProtectedBranches::CacheService.new(protected_branch.project).refresh
+ end
+
+ after(:build) do |obj, ctx|
+ next if ctx.ee || !ctx.default_access_level
+
+ obj.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER) if ctx.default_push_level
+ obj.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER) if ctx.default_merge_level
+ end
+
trait :create_branch_on_repository do
association :project, factory: [:project, :repository]
@@ -25,59 +39,63 @@ FactoryBot.define do
end
end
- trait :developers_can_push do
+ trait :maintainers_can_push do
transient do
default_push_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
- trait :developers_can_merge do
+ trait :maintainers_can_merge do
transient do
- default_merge_level { false }
+ default_push_level { false }
end
after(:build) do |protected_branch|
- protected_branch.merge_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
- trait :no_one_can_push do
+ trait :developers_can_push do
transient do
default_push_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
end
end
- trait :maintainers_can_push do
+ trait :developers_can_merge do
transient do
- default_push_level { false }
+ default_merge_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
end
end
- after(:build) do |protected_branch, evaluator|
- if evaluator.default_access_level && evaluator.default_push_level
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+ trait :no_one_can_push do
+ transient do
+ default_push_level { false }
end
- if evaluator.default_access_level && evaluator.default_merge_level
- protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+ after(:build) do |protected_branch|
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
end
end
trait :no_one_can_merge do
- after(:create) do |protected_branch|
- protected_branch.merge_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
+ transient do
+ default_merge_level { false }
+ end
+
+ after(:build) do |protected_branch|
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
end
end
end
diff --git a/spec/factories/users/ghost_user_migrations.rb b/spec/factories/users/ghost_user_migrations.rb
new file mode 100644
index 00000000000..0fe7cded4f3
--- /dev/null
+++ b/spec/factories/users/ghost_user_migrations.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ghost_user_migration, class: 'Users::GhostUserMigration' do
+ association :user
+ initiator_user { association(:user) }
+ hard_delete { false }
+ end
+end
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index 267ea9710b3..205b071a5d4 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -23,5 +23,9 @@ FactoryBot.define do
issue_type { :incident }
association :work_item_type, :default, :incident
end
+
+ trait :last_edited_by_user do
+ association :last_edited_by, factory: :user
+ end
end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 34ab48f67a8..db4d9125e6e 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -11,6 +11,9 @@ require_relative '../config/bundler_setup'
ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
+# Enable zero monkey patching mode before loading any other RSpec code.
+RSpec.configure(&:disable_monkey_patching!)
+
require 'active_support/dependencies'
require_relative '../config/initializers/0_inject_enterprise_edition_module'
require_relative '../config/settings'
@@ -29,13 +32,6 @@ end
ActiveSupport::XmlMini.backend = 'Nokogiri'
RSpec.configure do |config|
- unless ENV['CI']
- # Allow running `:focus` examples locally,
- # falling back to all tests when there is no `:focus` example.
- config.filter_run focus: true
- config.run_all_when_everything_filtered = true
- end
-
# Makes diffs show entire non-truncated values.
config.before(:each, unlimited_max_formatted_output_length: true) do |_example|
config.expect_with :rspec do |c|
diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb
index 0caa883fb5b..12f5e20e176 100644
--- a/spec/features/admin/admin_mode/workers_spec.rb
+++ b/spec/features/admin/admin_mode/workers_spec.rb
@@ -37,26 +37,56 @@ RSpec.describe 'Admin mode for workers', :request_store do
gitlab_enable_admin_mode_sign_in(user)
end
- it 'can delete user', :js do
- visit admin_user_path(user_to_delete)
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'can delete user', :js do
+ visit admin_user_path(user_to_delete)
+
+ click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
+
+ page.within '.modal-dialog' do
+ find("input[name='username']").send_keys(user_to_delete.name)
+ click_button 'Delete user'
+
+ wait_for_requests
+ end
- click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
+ expect(page).to have_content('The user is being deleted.')
- page.within '.modal-dialog' do
- find("input[name='username']").send_keys(user_to_delete.name)
- click_button 'Delete user'
+ # Perform jobs while logged out so that admin mode is only enabled in job metadata
+ execute_jobs_signed_out(user)
+
+ visit admin_user_path(user_to_delete)
+
+ expect(find('h1.page-title')).to have_content('(Blocked)')
+ end
+ end
- wait_for_requests
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
end
- expect(page).to have_content('The user is being deleted.')
+ it 'can delete user', :js do
+ visit admin_user_path(user_to_delete)
- # Perform jobs while logged out so that admin mode is only enabled in job metadata
- execute_jobs_signed_out(user)
+ click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
- visit admin_user_path(user_to_delete)
+ page.within '.modal-dialog' do
+ find("input[name='username']").send_keys(user_to_delete.name)
+ click_button 'Delete user'
+
+ wait_for_requests
+ end
+
+ expect(page).to have_content('The user is being deleted.')
- expect(page).to have_title('Not Found')
+ # Perform jobs while logged out so that admin mode is only enabled in job metadata
+ execute_jobs_signed_out(user)
+
+ visit admin_user_path(user_to_delete)
+
+ expect(page).to have_title('Not Found')
+ end
end
end
end
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index 24a10d3677d..33cf0e8c4f8 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Admin mode', :js do
open_top_nav_projects
within_top_nav do
- click_link('Your projects')
+ click_link('View all projects')
end
expect(page).to have_current_path(dashboard_projects_path)
@@ -99,7 +99,7 @@ RSpec.describe 'Admin mode', :js do
open_top_nav_projects
within_top_nav do
- click_link('Your projects')
+ click_link('View all projects')
end
expect(page).to have_current_path(dashboard_projects_path)
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 44fd21e510a..fe9fd01d3d5 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe "Admin Runners" do
context "when there are runners" do
context "with an instance runner" do
- let!(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
before do
visit admin_runners_path
@@ -50,7 +50,7 @@ RSpec.describe "Admin Runners" do
it 'shows an instance badge' do
within_runner_row(instance_runner.id) do
- expect(page).to have_selector '.badge', text: 'shared'
+ expect(page).to have_selector '.badge', text: s_('Runners|Instance')
end
end
end
@@ -66,9 +66,9 @@ RSpec.describe "Admin Runners" do
it 'has all necessary texts' do
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online runners 1"
- expect(page).to have_text "Offline runners 2"
- expect(page).to have_text "Stale runners 1"
+ expect(page).to have_text "#{s_('Runners|Online')} 1"
+ expect(page).to have_text "#{s_('Runners|Offline')} 2"
+ expect(page).to have_text "#{s_('Runners|Stale')} 1"
end
end
@@ -81,15 +81,17 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
within_runner_row(runner.id) do
- expect(find("[data-label='Jobs']")).to have_content '2'
+ expect(find("[data-testid='job-count']")).to have_content '2'
end
end
describe 'search' do
- before do
+ before_all do
create(:ci_runner, :instance, description: 'runner-foo')
create(:ci_runner, :instance, description: 'runner-bar')
+ end
+ before do
visit admin_runners_path
end
@@ -130,10 +132,12 @@ RSpec.describe "Admin Runners" do
end
describe 'filter by paused' do
- before do
+ before_all do
create(:ci_runner, :instance, description: 'runner-active')
create(:ci_runner, :instance, description: 'runner-paused', active: false)
+ end
+ before do
visit admin_runners_path
end
@@ -145,7 +149,7 @@ RSpec.describe "Admin Runners" do
end
it 'shows paused runners' do
- input_filtered_search_filter_is_only('Paused', 'Yes')
+ input_filtered_search_filter_is_only(s_('Runners|Paused'), 'Yes')
expect(page).to have_link('All 1')
@@ -154,7 +158,7 @@ RSpec.describe "Admin Runners" do
end
it 'shows active runners' do
- input_filtered_search_filter_is_only('Paused', 'No')
+ input_filtered_search_filter_is_only(s_('Runners|Paused'), 'No')
expect(page).to have_link('All 1')
@@ -164,15 +168,17 @@ RSpec.describe "Admin Runners" do
end
describe 'filter by status' do
- let!(:never_contacted) do
+ let_it_be(:never_contacted) do
create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil)
end
- before do
+ before_all do
create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.zone.now)
create(:ci_runner, :instance, description: 'runner-2', contacted_at: Time.zone.now)
create(:ci_runner, :instance, description: 'runner-offline', contacted_at: 1.week.ago)
+ end
+ before do
visit admin_runners_path
end
@@ -186,7 +192,7 @@ RSpec.describe "Admin Runners" do
end
it 'shows correct runner when status matches' do
- input_filtered_search_filter_is_only('Status', 'Online')
+ input_filtered_search_filter_is_only('Status', s_('Runners|Online'))
expect(page).to have_link('All 2')
@@ -197,7 +203,7 @@ RSpec.describe "Admin Runners" do
end
it 'shows correct runner when status is selected and search term is entered' do
- input_filtered_search_filter_is_only('Status', 'Online')
+ input_filtered_search_filter_is_only('Status', s_('Runners|Online'))
input_filtered_search_keys('runner-1')
expect(page).to have_link('All 1')
@@ -220,7 +226,7 @@ RSpec.describe "Admin Runners" do
expect(page).to have_content 'runner-never-contacted'
within_runner_row(never_contacted.id) do
- expect(page).to have_selector '.badge', text: 'never contacted'
+ expect(page).to have_selector '.badge', text: s_('Runners|Never contacted')
end
end
@@ -238,7 +244,7 @@ RSpec.describe "Admin Runners" do
end
describe 'filter by type' do
- before do
+ before_all do
create(:ci_runner, :project, description: 'runner-project', projects: [project])
create(:ci_runner, :group, description: 'runner-group', groups: [group])
end
@@ -308,7 +314,7 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
- input_filtered_search_filter_is_only('Paused', 'No')
+ input_filtered_search_filter_is_only(s_('Runners|Paused'), 'No')
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
@@ -345,7 +351,7 @@ RSpec.describe "Admin Runners" do
end
describe 'filter by tag' do
- before do
+ before_all do
create(:ci_runner, :instance, description: 'runner-blue', tag_list: ['blue'])
create(:ci_runner, :instance, description: 'runner-red', tag_list: ['red'])
end
@@ -464,7 +470,7 @@ RSpec.describe "Admin Runners" do
end
describe "Runner show page", :js do
- let(:runner) do
+ let_it_be(:runner) do
create(
:ci_runner,
description: 'runner-foo',
@@ -520,20 +526,25 @@ RSpec.describe "Admin Runners" do
end
describe "Runner edit page" do
- let(:runner) { create(:ci_runner, :project) }
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project) }
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:project_runner) { create(:ci_runner, :project) }
before do
- visit edit_admin_runner_path(runner)
+ visit edit_admin_runner_path(project_runner)
wait_for_requests
end
+ it_behaves_like 'submits edit runner form' do
+ let(:runner) { project_runner }
+ let(:runner_page_path) { admin_runner_path(project_runner) }
+ end
+
describe 'breadcrumbs' do
it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
- expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
+ expect(page).to have_link("##{project_runner.id} (#{project_runner.short_sha})")
expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_content("Edit")
end
end
@@ -541,7 +552,7 @@ RSpec.describe "Admin Runners" do
describe 'runner header', :js do
it 'contains the runner status, type and id' do
- expect(page).to have_content("never contacted specific Runner ##{runner.id} created")
+ expect(page).to have_content("#{s_('Runners|Never contacted')} Project Runner ##{project_runner.id} created")
end
end
@@ -556,7 +567,7 @@ RSpec.describe "Admin Runners" do
end
it 'redirects to runner page' do
- expect(current_url).to match(admin_runner_path(runner))
+ expect(current_url).to match(admin_runner_path(project_runner))
end
end
@@ -583,7 +594,7 @@ RSpec.describe "Admin Runners" do
describe 'enable/create' do
shared_examples 'assignable runner' do
it 'enables a runner for a project' do
- within '[data-testid="unassigned-projects"]' do
+ within find('[data-testid="unassigned-projects"] tr', text: project2.full_name) do
click_on 'Enable'
end
@@ -594,21 +605,21 @@ RSpec.describe "Admin Runners" do
end
end
- context 'with specific runner' do
- let(:runner) { create(:ci_runner, :project, projects: [project1]) }
+ context 'with project runner' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [project1]) }
before do
- visit edit_admin_runner_path(runner)
+ visit edit_admin_runner_path(project_runner)
end
it_behaves_like 'assignable runner'
end
context 'with locked runner' do
- let(:runner) { create(:ci_runner, :project, projects: [project1], locked: true) }
+ let(:locked_runner) { create(:ci_runner, :project, projects: [project1], locked: true) }
before do
- visit edit_admin_runner_path(runner)
+ visit edit_admin_runner_path(locked_runner)
end
it_behaves_like 'assignable runner'
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index faf13374719..d72259d91b3 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -189,7 +189,7 @@ RSpec.describe "Admin > Admin sees background migrations" do
visit admin_background_migrations_path
within '#content-body' do
- expect(page).to have_link('Learn more', href: help_page_path('development/database/batched_background_migrations'))
+ expect(page).to have_link('Learn more', href: help_page_path('user/admin_area/monitoring/background_migrations'))
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 8843e13026b..a5df142d188 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Account and Limit Settings' do
- page.within('.as-account-limit') do
+ page.within(find('[data-testid="account-limit"]')) do
uncheck 'Gravatar enabled'
click_button 'Save changes'
end
@@ -112,7 +112,7 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Maximum export size' do
- page.within('.as-account-limit') do
+ page.within(find('[data-testid="account-limit"]')) do
fill_in 'Maximum export size (MB)', with: 25
click_button 'Save changes'
end
@@ -122,7 +122,7 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Maximum import size' do
- page.within('.as-account-limit') do
+ page.within(find('[data-testid="account-limit"]')) do
fill_in 'Maximum import size (MB)', with: 15
click_button 'Save changes'
end
@@ -150,16 +150,20 @@ RSpec.describe 'Admin updates settings' do
it 'does not expose the setting' do
expect(page).to have_no_selector('#application_setting_deactivate_dormant_users')
end
+
+ it 'does not expose the setting' do
+ expect(page).to have_no_selector('#application_setting_deactivate_dormant_users_period')
+ end
end
context 'when not Gitlab.com' do
let(:dot_com?) { false }
- it 'change Dormant users' do
- expect(page).to have_unchecked_field('Deactivate dormant users after 90 days of inactivity')
+ it 'changes Dormant users' do
+ expect(page).to have_unchecked_field('Deactivate dormant users after a period of inactivity')
expect(current_settings.deactivate_dormant_users).to be_falsey
- page.within('.as-account-limit') do
+ page.within(find('[data-testid="account-limit"]')) do
check 'application_setting_deactivate_dormant_users'
click_button 'Save changes'
end
@@ -169,7 +173,22 @@ RSpec.describe 'Admin updates settings' do
page.refresh
expect(current_settings.deactivate_dormant_users).to be_truthy
- expect(page).to have_checked_field('Deactivate dormant users after 90 days of inactivity')
+ expect(page).to have_checked_field('Deactivate dormant users after a period of inactivity')
+ end
+
+ it 'change Dormant users period' do
+ expect(page).to have_field _('Period of inactivity (days)')
+
+ page.within(find('[data-testid="account-limit"]')) do
+ fill_in _('application_setting_deactivate_dormant_users_period'), with: '35'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+
+ page.refresh
+
+ expect(page).to have_field _('Period of inactivity (days)'), with: '35'
end
end
end
@@ -543,7 +562,7 @@ RSpec.describe 'Admin updates settings' do
it 'change Prometheus settings' do
page.within('.as-prometheus') do
- check 'Enable health and performance metrics endpoint'
+ check 'Enable GitLab Prometheus metrics endpoint'
click_button 'Save changes'
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index bc88b90a2dd..86acf5a05d4 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -205,7 +205,7 @@ RSpec.describe 'Admin::Users::User' do
it 'logs in as the user when impersonate is clicked' do
subject
- find('[data-qa-selector="user_menu"]').click # rubocop:disable QA/SelectorUsage
+ find('[data-testid="user-menu"]').click
expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eql(another_user.username)
end
@@ -241,7 +241,7 @@ RSpec.describe 'Admin::Users::User' do
it 'logs out of impersonated user back to original user' do
subject
- find('[data-qa-selector="user_menu"]').click # rubocop:disable QA/SelectorUsage
+ find('[data-testid="user-menu"]').click
expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eq(current_user.username)
end
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
index c44741b756b..b19e57c550c 100644
--- a/spec/features/clusters/create_agent_spec.rb
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -12,10 +12,11 @@ RSpec.describe 'Cluster agent registration', :js do
allow(Gitlab::Kas).to receive(:internal_url).and_return('kas.example.internal')
allow_next_instance_of(Gitlab::Kas::Client) do |client|
- allow(client).to receive(:list_agent_config_files).and_return([
- double(agent_name: 'example-agent-1', path: '.gitlab/agents/example-agent-1/config.yaml'),
- double(agent_name: 'example-agent-2', path: '.gitlab/agents/example-agent-2/config.yaml')
- ])
+ allow(client).to receive(:list_agent_config_files).and_return(
+ [
+ double(agent_name: 'example-agent-1', path: '.gitlab/agents/example-agent-1/config.yaml'),
+ double(agent_name: 'example-agent-2', path: '.gitlab/agents/example-agent-2/config.yaml')
+ ])
allow(client).to receive(:get_connected_agents).and_return([])
end
diff --git a/spec/features/commits/user_view_commits_spec.rb b/spec/features/commits/user_view_commits_spec.rb
index 5907534220d..f7fd3a6e209 100644
--- a/spec/features/commits/user_view_commits_spec.rb
+++ b/spec/features/commits/user_view_commits_spec.rb
@@ -3,14 +3,10 @@
require 'spec_helper'
RSpec.describe 'Commit > User view commits' do
- let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:user) { project.creator }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
- before do
- visit project_commits_path(project)
- end
-
- describe 'Commits List' do
+ shared_examples 'can view commits' do
it 'displays the correct number of commits per day in the header' do
expect(first('.js-commit-header').find('.commits-count').text).to eq('1 commit')
end
@@ -19,4 +15,51 @@ RSpec.describe 'Commit > User view commits' do
expect(page).to have_selector('#commits-list > li:nth-child(2) > ul', count: 1)
end
end
+
+ describe 'Commits List' do
+ context 'when project is public' do
+ let(:project) { create(:project, :public, :repository, group: group) }
+
+ before do
+ visit project_commits_path(project)
+ end
+
+ it_behaves_like 'can view commits'
+ end
+
+ context 'when project is public with private repository' do
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+
+ context 'and user is an inherited member from the group' do
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
+ visit project_commits_path(project)
+ end
+
+ it_behaves_like 'can view commits'
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is an inherited member from the group' do
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
+ visit project_commits_path(project)
+ end
+
+ it 'renders not found' do
+ expect(page).to have_title('Not Found')
+ expect(page).to have_content('Page Not Found')
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 7714783172f..488a4f84297 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -36,10 +36,6 @@ RSpec.describe 'Value Stream Analytics', :js do
wait_for_all_requests
end
- before do
- stub_feature_flags(use_vsa_aggregated_tables: false)
- end
-
context 'as an allowed user' do
context 'when project is new' do
before do
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index bf9f6895d24..48a6976f263 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Tooltips on .timeago dates', :js do
context 'on the activity tab' do
before do
Event.create!( project: project, author_id: user.id, action: :joined,
- updated_at: created_date, created_at: created_date)
+ updated_at: created_date, created_at: created_date)
sign_in user
visit user_activity_path(user)
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 3f89955b12b..08cb95979ac 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -41,7 +41,12 @@ RSpec.describe 'Dashboard > Milestones' do
first('.select2-result-label').click
end
- find('.js-new-project-item-link').click
+ a_el = find('.js-new-project-item-link')
+
+ expect(a_el).to have_content('New Milestone in ')
+ expect(a_el).to have_no_content('New New Milestone in ')
+
+ a_el.click
expect(page).to have_current_path(new_group_milestone_path(group), ignore_query: true)
end
diff --git a/spec/features/dashboard/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb
index d593031590e..a0fa53b761b 100644
--- a/spec/features/dashboard/todos/todos_sorting_spec.rb
+++ b/spec/features/dashboard/todos/todos_sorting_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Dashboard > User sorts todos' do
create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago, updated_at: 2.minutes.ago)
create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago, updated_at: 2.hours.ago)
create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago,
- updated_at: 1.hour.ago)
+ updated_at: 1.hour.ago)
merge_request_1.labels << label_1
issue_3.labels << label_1
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
index ebb57b37918..0bb43343ecd 100644
--- a/spec/features/discussion_comments/issue_spec.rb
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Thread Comments Issue', :js do
let(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(remove_user_attributes_projects: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
index a90ff3721d3..4fa82de3b4b 100644
--- a/spec/features/discussion_comments/merge_request_spec.rb
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Thread Comments Merge Request', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
+ stub_feature_flags(remove_user_attributes_projects: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb
index b98c94b030d..ada03726c97 100644
--- a/spec/features/groups/group_runners_spec.rb
+++ b/spec/features/groups/group_runners_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe "Group Runners" do
it 'shows a group badge' do
within_runner_row(group_runner.id) do
- expect(page).to have_selector '.badge', text: 'group'
+ expect(page).to have_selector '.badge', text: s_('Runners|Group')
end
end
@@ -101,9 +101,9 @@ RSpec.describe "Group Runners" do
let(:runner) { project_runner }
end
- it 'shows a project (specific) badge' do
+ it 'shows a project badge' do
within_runner_row(project_runner.id) do
- expect(page).to have_selector '.badge', text: 'specific'
+ expect(page).to have_selector '.badge', text: s_('Runners|Project')
end
end
@@ -137,52 +137,38 @@ RSpec.describe "Group Runners" do
focus_filtered_search
page.within(search_bar_selector) do
- expect(page).to have_link('Paused')
+ expect(page).to have_link(s_('Runners|Paused'))
expect(page).to have_content('Status')
end
end
end
end
- describe "Group runner edit page", :js do
- let!(:runner) do
- create(:ci_runner, :group, groups: [group], description: 'runner-foo', contacted_at: Time.zone.now)
+ describe "Group runner show page", :js do
+ let!(:group_runner) do
+ create(:ci_runner, :group, groups: [group], description: 'runner-foo')
end
it 'user views runner details' do
- visit group_runner_path(group, runner)
+ visit group_runner_path(group, group_runner)
expect(page).to have_content "#{s_('Runners|Description')} runner-foo"
end
+ end
- it 'user edits the runner to be protected' do
- visit edit_group_runner_path(group, runner)
-
- expect(page.find_field('runner[access_level]')).not_to be_checked
-
- check 'runner_access_level'
- click_button _('Save changes')
-
- expect(page).to have_content "#{s_('Runners|Configuration')} #{s_('Runners|Protected')}"
+ describe "Group runner edit page", :js do
+ let!(:group_runner) do
+ create(:ci_runner, :group, groups: [group])
end
- context 'when a runner has a tag' do
- before do
- runner.update!(tag_list: ['tag1'])
- end
-
- it 'user edits runner not to run untagged jobs' do
- visit edit_group_runner_path(group, runner)
-
- page.find_field('runner[tag_list]').set('tag1, tag2')
-
- uncheck 'runner_run_untagged'
- click_button _('Save changes')
+ before do
+ visit edit_group_runner_path(group, group_runner)
+ wait_for_requests
+ end
- # Tags can be in any order
- expect(page).to have_content /#{s_('Runners|Tags')}.*tag1/
- expect(page).to have_content /#{s_('Runners|Tags')}.*tag2/
- end
+ it_behaves_like 'submits edit runner form' do
+ let(:runner) { group_runner }
+ let(:runner_page_path) { group_runner_path(group, group_runner) }
end
end
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index b4faa3ce0dd..b3fb563a202 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -14,10 +14,12 @@ RSpec.describe 'Group navbar' do
before do
insert_package_nav(_('Kubernetes'))
+ insert_after_nav_item(_('Analytics'), new_nav_item: settings_for_maintainer_nav_item) if Gitlab.ee?
stub_config(dependency_proxy: { enabled: false })
stub_config(registry: { enabled: false })
stub_feature_flags(harbor_registry_integration: false)
+ stub_feature_flags(observability_group_tab: false)
stub_group_wikis(false)
group.add_maintainer(user)
sign_in(user)
@@ -48,7 +50,7 @@ RSpec.describe 'Group navbar' do
if Gitlab.ee?
insert_customer_relations_nav(_('Analytics'))
else
- insert_customer_relations_nav(_('Packages & Registries'))
+ insert_customer_relations_nav(_('Packages and registries'))
end
visit group_path(group)
@@ -80,7 +82,11 @@ RSpec.describe 'Group navbar' do
end
context 'when harbor registry is available' do
+ let(:harbor_integration) { create(:harbor_integration, group: group, project: nil) }
+
before do
+ group.update!(harbor_integration: harbor_integration)
+
stub_feature_flags(harbor_registry_integration: true)
insert_harbor_registry_nav(_('Package Registry'))
@@ -90,4 +96,16 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
+
+ context 'when observability tab is enabled' do
+ before do
+ stub_feature_flags(observability_group_tab: true)
+
+ insert_observability_nav
+
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
end
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index 98dc534f54e..7f3f5775559 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Packages & Registries settings' do
+RSpec.describe 'Group Package and registry settings' do
include WaitForRequests
let(:user) { create(:user) }
@@ -25,7 +25,7 @@ RSpec.describe 'Group Packages & Registries settings' do
settings_menu = find_settings_menu
- expect(settings_menu).not_to have_content 'Packages & Registries'
+ expect(settings_menu).not_to have_content 'Packages and registries'
end
it 'renders 404 when navigating to page' do
@@ -40,57 +40,58 @@ RSpec.describe 'Group Packages & Registries settings' do
visit group_path(group)
settings_menu = find_settings_menu
- expect(settings_menu).to have_content 'Packages & Registries'
+ expect(settings_menu).to have_content 'Packages and registries'
end
it 'has a page title set' do
visit_settings_page
- expect(page).to have_title _('Packages & Registries')
+ expect(page).to have_title _('Package and registry settings')
end
it 'sidebar menu is open' do
visit_settings_page
sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Packages & Registries')
+ expect(sidebar).to have_link _('Packages and registries')
end
it 'has a Duplicate packages section', :js do
visit_settings_page
expect(page).to have_content('Duplicate packages')
+ expect(page).to have_content('Allow duplicates')
+ expect(page).to have_content('Exceptions')
end
it 'automatically saves changes to the server', :js do
visit_settings_page
+ wait_for_requests
within '[data-testid="maven-settings"]' do
- expect(page).to have_content('Reject packages with the same name and version')
- expect(page).not_to have_content('Exceptions')
+ expect(page).to have_field _('Exceptions'), disabled: true
- find('.gl-toggle').click
+ click_button class: 'gl-toggle'
- expect(page).to have_content('Exceptions')
+ expect(page).to have_field _('Exceptions'), disabled: false
visit_settings_page
- expect(page).to have_content('Exceptions')
+ expect(page).to have_field _('Exceptions'), disabled: false
end
end
it 'shows an error on wrong regex', :js do
visit_settings_page
+ wait_for_requests
within '[data-testid="maven-settings"]' do
- expect(page).to have_content('Reject packages with the same name and version')
+ click_button class: 'gl-toggle'
- find('.gl-toggle').click
-
- fill_in 'Exceptions', with: ')'
+ fill_in _('Exceptions'), with: ')'
# simulate blur event
- find('#maven-duplicated-settings-regex-input').native.send_keys(:tab)
+ send_keys(:tab)
end
expect(page).to have_content('is an invalid regexp')
@@ -99,13 +100,16 @@ RSpec.describe 'Group Packages & Registries settings' do
context 'in a sub group' do
it 'works correctly', :js do
visit_sub_group_settings_page
+ wait_for_requests
within '[data-testid="maven-settings"]' do
- expect(page).to have_content('Reject packages with the same name and version')
+ expect(page).to have_content('Allow duplicates')
+
+ expect(page).to have_field _('Exceptions'), disabled: true
- find('.gl-toggle').click
+ click_button class: 'gl-toggle'
- expect(page).to have_content('Exceptions')
+ expect(page).to have_field _('Exceptions'), disabled: false
end
end
end
diff --git a/spec/features/groups/settings/user_searches_in_settings_spec.rb b/spec/features/groups/settings/user_searches_in_settings_spec.rb
index 998c3d2ca3f..fe0dd7cec9a 100644
--- a/spec/features/groups/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/groups/settings/user_searches_in_settings_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'User searches group settings', :js do
it_behaves_like 'can search settings', 'Variables', 'Auto DevOps'
end
- context 'in Packages & Registries page' do
+ context 'in Packages and registries page' do
before do
visit group_settings_packages_and_registries_path(group)
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c93ed01b873..4e02f6f7ca2 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -517,75 +517,4 @@ RSpec.describe 'Group' do
fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm'
end
-
- describe 'storage_enforcement_banner', :js do
- let_it_be(:group) { create(:group) }
- let_it_be_with_refind(:user) { create(:user) }
-
- before do
- group.add_owner(user)
- sign_in(user)
- end
-
- context 'with storage_enforcement_date set' do
- let_it_be(:storage_enforcement_date) { Date.today + 30 }
-
- before do
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- end
-
- it 'displays the banner in the group page' do
- visit group_path(group)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
-
- it 'does not display the banner in a paid group page' do
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:paid?).and_return(true)
- end
- visit group_path(group)
- expect_page_not_to_have_storage_enforcement_banner
- end
-
- it 'does not display the banner if user has previously closed unless threshold has changed' do
- visit group_path(group)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- find('.js-storage-enforcement-banner [data-testid="close-icon"]').click
- wait_for_requests
- page.refresh
- expect_page_not_to_have_storage_enforcement_banner
-
- storage_enforcement_date = Date.today + 13
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- page.refresh
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
- end
-
- context 'with storage_enforcement_date not set' do
- before do
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:storage_enforcement_date).and_return(nil)
- end
- end
-
- it 'does not display the banner in the group page' do
- stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
- visit group_path(group)
- expect_page_not_to_have_storage_enforcement_banner
- end
- end
- end
-
- def expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- expect(page).to have_text "Effective #{storage_enforcement_date}, namespace storage limits will apply"
- end
-
- def expect_page_not_to_have_storage_enforcement_banner
- expect(page).not_to have_text "namespace storage limits will apply"
- end
end
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
index db98f58240d..e64c19d4708 100644
--- a/spec/features/help_dropdown_spec.rb
+++ b/spec/features/help_dropdown_spec.rb
@@ -51,7 +51,8 @@ RSpec.describe "Help Dropdown", :js do
visit root_path
end
- it 'renders correct version badge variant' do
+ it 'renders correct version badge variant',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/369850' do
page.within '.header-help' do
find('.header-help-dropdown-toggle').click
diff --git a/spec/features/ide/user_commits_changes_spec.rb b/spec/features/ide/user_commits_changes_spec.rb
index e1e586a4f18..04b215710b3 100644
--- a/spec/features/ide/user_commits_changes_spec.rb
+++ b/spec/features/ide/user_commits_changes_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'IDE user commits changes', :js do
let(:user) { project.first_owner }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
sign_in(user)
ide_visit(project)
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 8f4668d49ee..8a95d7c5544 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe 'IDE merge request', :js do
let(:user) { project.first_owner }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
sign_in(user)
visit(merge_request_path(merge_request))
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
index 2505ab0afee..c7c740c2293 100644
--- a/spec/features/ide_spec.rb
+++ b/spec/features/ide_spec.rb
@@ -4,12 +4,14 @@ require 'spec_helper'
RSpec.describe 'IDE', :js do
describe 'sub-groups' do
+ let(:ide_iframe_selector) { '#ide iframe' }
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
before do
+ stub_feature_flags(vscode_web_ide: vscode_ff)
subgroup_project.add_maintainer(user)
sign_in(user)
@@ -20,8 +22,28 @@ RSpec.describe 'IDE', :js do
wait_for_requests
end
- it 'loads project in web IDE' do
- expect(page).to have_selector('.context-header', text: subgroup_project.name)
+ context 'with vscode feature flag on' do
+ let(:vscode_ff) { true }
+
+ it 'loads project in Web IDE' do
+ iframe = find(ide_iframe_selector)
+
+ page.within_frame(iframe) do
+ expect(page).to have_selector('.title', text: subgroup_project.name.upcase)
+ end
+ end
+ end
+
+ context 'with vscode feature flag off' do
+ let(:vscode_ff) { false }
+
+ it 'loads project in legacy Web IDE' do
+ expect(page).to have_selector('.context-header', text: subgroup_project.name)
+ end
+
+ it 'does not load new Web IDE' do
+ expect(page).not_to have_selector(ide_iframe_selector)
+ end
end
end
end
diff --git a/spec/features/incidents/incident_timeline_events_spec.rb b/spec/features/incidents/incident_timeline_events_spec.rb
index e39f348013c..6db9f87d6f2 100644
--- a/spec/features/incidents/incident_timeline_events_spec.rb
+++ b/spec/features/incidents/incident_timeline_events_spec.rb
@@ -17,20 +17,20 @@ RSpec.describe 'Incident timeline events', :js do
visit project_issues_incident_path(project, incident)
wait_for_requests
- click_link 'Timeline'
+ click_link s_('Incident|Timeline')
end
context 'when add event is clicked' do
it 'submits event data when save is clicked' do
- click_button 'Add new timeline event'
+ click_button s_('Incident|Add new timeline event')
expect(page).to have_selector('.common-note-form')
- fill_in 'Description', with: 'Event note goes here'
+ fill_in _('Description'), with: 'Event note goes here'
fill_in 'timeline-input-hours', with: '07'
fill_in 'timeline-input-minutes', with: '25'
- click_button 'Save'
+ click_button _('Save')
expect(page).to have_selector('.incident-timeline-events')
@@ -41,30 +41,62 @@ RSpec.describe 'Incident timeline events', :js do
end
end
- context 'when delete event is clicked' do
+ context 'when edit is clicked' do
before do
click_button 'Add new timeline event'
- fill_in 'Description', with: 'Event note to delete'
- click_button 'Save'
+ fill_in 'Description', with: 'Event note to edit'
+ click_button _('Save')
+ end
+
+ it 'shows the confirmation modal and edits the event' do
+ click_button _('More actions')
+
+ page.within '.gl-new-dropdown-contents' do
+ expect(page).to have_content(_('Edit'))
+ page.find('.gl-new-dropdown-item-text-primary', text: _('Edit')).click
+ end
+
+ expect(page).to have_selector('.common-note-form')
+
+ fill_in _('Description'), with: 'Event note goes here'
+ fill_in 'timeline-input-hours', with: '07'
+ fill_in 'timeline-input-minutes', with: '25'
+
+ click_button _('Save')
+
+ wait_for_requests
+
+ page.within '.timeline-event-note' do
+ expect(page).to have_content('Event note goes here')
+ expect(page).to have_content('07:25')
+ end
+ end
+ end
+
+ context 'when delete is clicked' do
+ before do
+ click_button s_('Incident|Add new timeline event')
+ fill_in _('Description'), with: 'Event note to delete'
+ click_button _('Save')
end
it 'shows the confirmation modal and deletes the event' do
- click_button 'More actions'
+ click_button _('More actions')
- page.within '.gl-new-dropdown-item-text-wrapper' do
- expect(page).to have_content('Delete')
+ page.within '.gl-new-dropdown-contents' do
+ expect(page).to have_content(_('Delete'))
page.find('.gl-new-dropdown-item-text-primary', text: 'Delete').click
end
page.within '.modal' do
- expect(page).to have_content('Delete event')
+ expect(page).to have_content(s_('Incident|Delete event'))
end
- click_button 'Delete event'
+ click_button s_('Incident|Delete event')
wait_for_requests
- expect(page).to have_content('No timeline items have been added yet.')
+ expect(page).to have_content(s_('Incident|No timeline items have been added yet.'))
end
end
end
diff --git a/spec/features/incidents/user_uses_quick_actions_spec.rb b/spec/features/incidents/user_uses_quick_actions_spec.rb
new file mode 100644
index 00000000000..fce9eadd42f
--- /dev/null
+++ b/spec/features/incidents/user_uses_quick_actions_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Incidents > User uses quick actions', :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+
+ describe 'incident-only commands' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue, reload: true) { create(:incident, project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ wait_for_all_requests
+ end
+
+ after do
+ wait_for_requests
+ end
+
+ it_behaves_like 'timeline quick action'
+ end
+end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 1baa97096d9..34990a53b51 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
let(:group_invite) { group.group_members.invite.last }
before do
+ stub_feature_flags(arkose_labs_signup_challenge: false)
stub_application_setting(require_admin_approval_after_user_signup: false)
project.add_maintainer(owner)
group.add_owner(owner)
@@ -245,6 +246,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
before do
stub_feature_flags(soft_email_confirmation: false)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
+ stub_feature_flags(identity_verification: false)
end
it 'signs up and redirects to the group activity page' do
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index ae9c8d31c02..9d46b3a274e 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe "Jira", :js do
before do
remotelink = double(:remotelink, all: [], build: double(save!: true))
+ stub_feature_flags(remove_user_attributes_projects: false)
+
stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
allow_next_instance_of(JIRA::Resource::Issue) do |instance|
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 1e8b9b6b60b..a385e8a5fd0 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -18,6 +18,10 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
end
end
+ before do
+ stub_feature_flags(moved_mr_sidebar: false)
+ end
+
describe 'as a user with access to the project' do
before do
project.add_maintainer(user)
@@ -26,14 +30,16 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
end
it 'shows a button to resolve all threads by creating a new issue' do
+ find('.discussions-counter .dropdown-toggle').click
+
within('.discussions-counter') do
- expect(page).to have_selector resolve_all_discussions_link_selector( title: "Create issue to resolve all threads" )
+ expect(page).to have_link(_("Create issue to resolve all threads"), href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid))
end
end
context 'resolving the thread' do
before do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
it 'hides the link for creating a new issue' do
@@ -44,6 +50,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
context 'creating an issue for threads' do
before do
+ find('.discussions-counter .dropdown-toggle').click
find(resolve_all_discussions_link_selector).click
end
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index 0de15d3d304..5ff61a52b21 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'Resolve an open thread in a merge request by creating an issue',
context 'resolving the thread' do
before do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
it 'hides the link for creating a new issue' do
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 2af54e51bb7..a253e6f4c86 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -68,8 +68,12 @@ RSpec.describe 'Issue Detail', :js do
end
context 'when edited by a user who is later deleted' do
+ let(:user_to_be_deleted) { create(:user) }
+
before do
- sign_in(user)
+ project.add_developer(user_to_be_deleted)
+
+ sign_in(user_to_be_deleted)
visit project_issue_path(project, issue)
wait_for_requests
@@ -78,8 +82,9 @@ RSpec.describe 'Issue Detail', :js do
click_button 'Save changes'
wait_for_requests
- Users::DestroyService.new(user).execute(user)
+ Users::DestroyService.new(user_to_be_deleted).execute(user_to_be_deleted)
+ sign_in(user)
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 8819f085a5f..6fa8a52a9c5 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -302,7 +302,9 @@ RSpec.describe 'Issue Sidebar' do
context 'sidebar', :js do
it 'finds issue copy forwarding email' do
- expect(find('[data-qa-selector="copy-forward-email"]').text).to eq "Issue email: #{issue.creatable_note_email_address(user)}" # rubocop:disable QA/SelectorUsage
+ expect(
+ find('[data-testid="copy-forward-email"]').text
+ ).to eq "Issue email: #{issue.creatable_note_email_address(user)}"
end
end
@@ -338,7 +340,7 @@ RSpec.describe 'Issue Sidebar' do
end
it 'does not find issue email' do
- expect(page).not_to have_selector('[data-qa-selector="copy-forward-email"]') # rubocop:disable QA/SelectorUsage
+ expect(page).not_to have_selector('[data-testid="copy-forward-email"]')
end
end
end
diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb
index e08410efc0b..e4da2f67516 100644
--- a/spec/features/issues/resource_label_events_spec.rb
+++ b/spec/features/issues/resource_label_events_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'List issue resource label events', :js do
let!(:event) { create(:resource_label_event, user: user, issue: issue, label: label) }
before do
+ stub_feature_flags(remove_user_attributes_projects: false)
visit project_issue_path(project, issue)
wait_for_requests
end
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index e29911e3263..b96490bd7e7 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -269,7 +269,7 @@ RSpec.describe "User creates issue" do
end
it 'hides the weight input' do
- expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage
+ expect(page).not_to have_selector('[data-testid="issuable-weight-input"]')
end
it 'shows the incident help text' do
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index c86a2c32e2d..8ed56108f00 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User interacts with awards' do
+ include MobileHelpers
+
let(:user) { create(:user) }
describe 'User interacts with awards in an issue', :js do
@@ -130,6 +132,7 @@ RSpec.describe 'User interacts with awards' do
end
it 'allows adding a new emoji' do
+ resize_window(1200, 800)
page.within('.note-actions') do
find('.note-emoji-button').click
end
@@ -140,6 +143,7 @@ RSpec.describe 'User interacts with awards' do
expect(page).to have_emoji('8ball')
end
expect(note.reload.award_emoji.size).to eq(2)
+ restore_window_size
end
context 'when the project is archived' do
diff --git a/spec/features/issues/user_sees_empty_state_spec.rb b/spec/features/issues/user_sees_empty_state_spec.rb
index b43ba01606a..0e2a7cb4358 100644
--- a/spec/features/issues/user_sees_empty_state_spec.rb
+++ b/spec/features/issues/user_sees_empty_state_spec.rb
@@ -40,8 +40,8 @@ RSpec.describe 'Issues > User sees empty state', :js do
it 'user sees empty state' do
visit project_issues_path(project)
- expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project')
- expect(page).to have_content('Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.')
+ expect(page).to have_content('Use issues to collaborate on ideas, solve problems, and plan work')
+ expect(page).to have_content('Learn more about issues.')
expect(page).to have_content('New issue')
end
diff --git a/spec/features/issues/user_sorts_issue_comments_spec.rb b/spec/features/issues/user_sorts_issue_comments_spec.rb
index 555f8827374..4b38ce329b8 100644
--- a/spec/features/issues/user_sorts_issue_comments_spec.rb
+++ b/spec/features/issues/user_sorts_issue_comments_spec.rb
@@ -16,7 +16,9 @@ RSpec.describe 'Comment sort direction' do
it 'saves sort order' do
# open dropdown, and select 'Newest first'
page.within('.issuable-details') do
+ find('#discussion-preferences-dropdown').click
click_button('Oldest first')
+ find('#discussion-preferences-dropdown').click
click_button('Newest first')
end
diff --git a/spec/features/jira_oauth_provider_authorize_spec.rb b/spec/features/jira_oauth_provider_authorize_spec.rb
index a216d2d44b2..eb26440aff9 100644
--- a/spec/features/jira_oauth_provider_authorize_spec.rb
+++ b/spec/features/jira_oauth_provider_authorize_spec.rb
@@ -10,10 +10,10 @@ RSpec.describe 'JIRA OAuth Provider' do
sign_in(user)
visit oauth_jira_dvcs_authorize_path(client_id: application.uid,
- redirect_uri: oauth_jira_dvcs_callback_url,
- response_type: 'code',
- state: 'my_state',
- scope: 'read_user')
+ redirect_uri: oauth_jira_dvcs_callback_url,
+ response_type: 'code',
+ state: 'my_state',
+ scope: 'read_user')
end
it_behaves_like 'Secure OAuth Authorizations'
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index 9eff02a8c1b..08f9b8eda13 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -263,6 +263,10 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
expect(doc).to parse_task_lists
end
+ aggregate_failures 'MathFilter' do
+ expect(doc).to parse_math
+ end
+
aggregate_failures 'InlineDiffFilter' do
expect(doc).to parse_inline_diffs
end
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index fafaea8ac68..bccdc3c4c62 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe 'Merge request > Batch comments', :js do
end
before do
+ stub_feature_flags(moved_mr_sidebar: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index f21929e5275..fd33731cb7b 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(remove_user_attributes_projects: false)
project.add_maintainer(user)
sign_in(user)
@@ -256,9 +257,7 @@ RSpec.describe 'User comments on a diff', :js do
click_button('Delete comment', match: :first)
end
- page.within('.merge-request-tabs') do
- find('.notes-tab').click
- end
+ find('.notes-tab', visible: true).click
wait_for_requests
diff --git a/spec/features/merge_request/user_comments_on_merge_request_spec.rb b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
index dbcfc2b968f..ec1e2fea851 100644
--- a/spec/features/merge_request/user_comments_on_merge_request_spec.rb
+++ b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'User comments on a merge request', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(remove_user_attributes_projects: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index c8b22bb3125..0ae4ef18649 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -1,111 +1,164 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'spec_helper'
-RSpec.describe "User creates a merge request", :js do
+RSpec.describe 'User creates a merge request', :js do
include ProjectForksHelper
- let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { create(:user) }
-
- let(:title) { "Some feature" }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it "creates a merge request" do
- visit(project_new_merge_request_path(project))
+ shared_examples 'creates a merge request' do
+ specify do
+ visit(project_new_merge_request_path(project))
- find(".js-source-branch").click
- click_link("fix")
+ compare_source_and_target('fix', 'feature')
- find(".js-target-branch").click
- click_link("feature")
+ page.within('.merge-request-form') do
+ expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
+ end
- click_button("Compare branches")
+ fill_in('Title', with: title)
+ click_button('Create merge request')
- page.within('.merge-request-form') do
- expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
+ page.within('.merge-request') do
+ expect(page).to have_content(title)
+ end
end
+ end
- fill_in("Title", with: title)
- click_button("Create merge request")
+ shared_examples 'renders not found' do
+ specify do
+ visit project_new_merge_request_path(project)
- page.within(".merge-request") do
- expect(page).to have_content(title)
+ expect(page).to have_title('Not Found')
+ expect(page).to have_content('Page Not Found')
end
end
- context "XSS branch name exists" do
+ context 'when user is a direct project member' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:title) { 'Some feature' }
+
before do
- project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", "master")
+ project.add_maintainer(user)
+ sign_in(user)
end
- it "doesn't execute the dodgy branch name" do
- visit(project_new_merge_request_path(project))
+ it_behaves_like 'creates a merge request'
- find(".js-source-branch").click
- click_link("<img/src='x'/onerror=alert('oops')>")
+ context 'with XSS branch name' do
+ before do
+ project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", 'master')
+ end
- find(".js-target-branch").click
- click_link("feature")
+ it 'does not execute the suspicious branch name' do
+ visit(project_new_merge_request_path(project))
- click_button("Compare branches")
+ compare_source_and_target("<img/src='x'/onerror=alert('oops')>", 'feature')
- expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
+ expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
+ end
end
- end
- context "to a forked project" do
- let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
+ context 'to a forked project' do
+ let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
+
+ it 'creates a merge request', :sidekiq_might_not_need_inline do
+ visit(project_new_merge_request_path(forked_project))
+
+ expect(page).to have_content('Source branch').and have_content('Target branch')
+ expect(find('#merge_request_target_project_id', visible: false).value).to eq(project.id.to_s)
- it "creates a merge request", :sidekiq_might_not_need_inline do
- visit(project_new_merge_request_path(forked_project))
+ click_button('Compare branches and continue')
- expect(page).to have_content("Source branch").and have_content("Target branch")
- expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s)
+ expect(page).to have_content('You must select source and target branch')
- click_button("Compare branches and continue")
+ first('.js-source-project').click
+ first('.dropdown-source-project a', text: forked_project.full_path)
- expect(page).to have_content("You must select source and target branch")
+ first('.js-target-project').click
+ first('.dropdown-target-project a', text: project.full_path)
- first(".js-source-project").click
- first(".dropdown-source-project a", text: forked_project.full_path)
+ first('.js-source-branch').click
- first(".js-target-project").click
- first(".dropdown-target-project a", text: project.full_path)
+ wait_for_requests
- first(".js-source-branch").click
+ source_branch = 'fix'
- wait_for_requests
+ first('.js-source-branch-dropdown .dropdown-content a', text: source_branch).click
- source_branch = "fix"
+ click_button('Compare branches and continue')
- first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click
+ expect(page).to have_text _('New merge request')
- click_button("Compare branches and continue")
+ page.within('form#new_merge_request') do
+ fill_in('Title', with: title)
+ end
- expect(page).to have_text _('New merge request')
+ expect(find('.js-assignee-search')['data-project-id']).to eq(project.id.to_s)
+ find('.js-assignee-search').click
- page.within("form#new_merge_request") do
- fill_in("Title", with: title)
+ page.within('.dropdown-menu-user') do
+ expect(page).to have_content('Unassigned')
+ .and have_content(user.name)
+ .and have_content(project.users.first.name)
+ end
+ find('.js-assignee-search').click
+
+ click_button('Create merge request')
+
+ expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master")
+ end
+ end
+ end
+
+ context 'when user is an inherited member from the group' do
+ let_it_be(:group) { create(:group, :public) }
+
+ let(:user) { create(:user) }
+
+ context 'when project is public and merge requests are private' do
+ let_it_be(:project) do
+ create(:project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED)
end
- expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
- find('.js-assignee-search').click
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
+ end
- page.within(".dropdown-menu-user") do
- expect(page).to have_content("Unassigned")
- .and have_content(user.name)
- .and have_content(project.users.first.name)
+ it_behaves_like 'renders not found'
end
- find('.js-assignee-search').click
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
- click_button("Create merge request")
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
+ end
- expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master")
+ it_behaves_like 'renders not found'
+ end
end
end
+
+ private
+
+ def compare_source_and_target(source_branch, target_branch)
+ find('.js-source-branch').click
+ click_link(source_branch)
+
+ find('.js-target-branch').click
+ click_link(target_branch)
+
+ click_button('Compare branches')
+ end
end
diff --git a/spec/features/merge_request/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb
index 0b4b9d7452a..4ac25ea7ae0 100644
--- a/spec/features/merge_request/user_edits_merge_request_spec.rb
+++ b/spec/features/merge_request/user_edits_merge_request_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'User edits a merge request', :js do
- include Select2Helper
-
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -89,7 +87,12 @@ RSpec.describe 'User edits a merge request', :js do
it 'allows user to change target branch' do
expect(page).to have_content('From master into feature')
- select2('merge-test', from: '#merge_request_target_branch')
+ first('.js-target-branch').click
+
+ wait_for_requests
+
+ first('.js-target-branch-dropdown a', text: 'merge-test').click
+
click_button('Save changes')
expect(page).to have_content("requested to merge #{merge_request.source_branch} into merge-test")
@@ -101,7 +104,7 @@ RSpec.describe 'User edits a merge request', :js do
it 'does not allow user to change target branch' do
expect(page).to have_content('From master into feature')
- expect(page).not_to have_selector('.select2-container')
+ expect(page).not_to have_selector('.js-target-branch.js-compare-dropdown')
end
end
end
diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb
index c64c761b8d1..9fb85957979 100644
--- a/spec/features/merge_request/user_manages_subscription_spec.rb
+++ b/spec/features/merge_request/user_manages_subscription_spec.rb
@@ -45,21 +45,21 @@ RSpec.describe 'User manages subscription', :js do
click_button 'Toggle dropdown'
- expect(page).to have_content('Turn on notifications')
- click_button 'Turn on notifications'
+ expect(page).to have_selector('.gl-toggle:not(.is-checked)')
+ find('[data-testid="notifications-toggle"] .gl-toggle').click
wait_for_requests
click_button 'Toggle dropdown'
- expect(page).to have_content('Turn off notifications')
- click_button 'Turn off notifications'
+ expect(page).to have_selector('.gl-toggle.is-checked')
+ find('[data-testid="notifications-toggle"] .gl-toggle').click
wait_for_requests
click_button 'Toggle dropdown'
- expect(page).to have_content('Turn on notifications')
+ expect(page).to have_selector('.gl-toggle:not(.is-checked)')
end
end
end
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index e09ec11f095..332426de07e 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -15,6 +15,10 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
diff_refs: merge_request.diff_refs)
end
+ before do
+ stub_feature_flags(moved_mr_sidebar: false)
+ end
+
context 'no threads' do
before do
project.add_maintainer(user)
@@ -62,7 +66,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to mark thread as resolved' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
expect(page).to have_selector('.discussion-body', visible: false)
@@ -78,7 +82,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to unresolve thread' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
click_button 'Unresolve thread'
end
@@ -90,7 +94,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
describe 'resolved thread' do
before do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
visit_merge_request
@@ -190,7 +194,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to resolve from reply form without a comment' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
page.within '.discussions-counter' do
@@ -225,7 +229,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'hides jump to next button when all resolved' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
expect(page).to have_selector('.discussion-next-btn', visible: false)
@@ -320,7 +324,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to mark all threads as resolved' do
page.all('.discussion-reply-holder', count: 2).each do |reply_holder|
page.within reply_holder do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
end
@@ -331,7 +335,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to quickly scroll to next unresolved thread' do
page.within('.discussion-reply-holder', match: :first) do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
page.within '.discussions-counter' do
@@ -402,7 +406,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to mark thread as resolved' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
end
page.within '.diff-content .note' do
@@ -416,7 +420,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to unresolve thread' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
click_button 'Unresolve thread'
end
@@ -443,7 +447,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'allows user to comment & unresolve thread' do
page.within '.diff-content' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
find_field('Reply…').click
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index c02149eed87..63ac7862b06 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
context 'with stop action' do
let(:manual) do
create(:ci_build, :manual, pipeline: pipeline,
- name: 'close_app', environment: environment.name)
+ name: 'close_app', environment: environment.name)
end
before do
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 1d3effd4a2a..c2a0e528ea7 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -219,7 +219,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
shared_examples 'pipeline widget' do
it 'shows head pipeline information', :sidekiq_might_not_need_inline do
within '.ci-widget-content' do
- expect(page).to have_content("Detached merge request pipeline ##{pipeline.id} pending for #{pipeline.short_sha}")
+ expect(page).to have_content("Merge request pipeline ##{pipeline.id} pending for #{pipeline.short_sha}")
end
end
end
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index 563120fc8b7..ca102913369 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -7,7 +7,7 @@ require 'spec_helper'
# for example, adding quick actions when creating the issue and checking DateTime formats on UI.
# Because this kind of spec takes more time to run there is no need to add new ones
# for each existing quick action unless they test something not tested by existing tests.
-RSpec.describe 'Merge request > User uses quick actions', :js do
+RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_redis_caching do
include Spec::Support::Helpers::Features::NotesHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb b/spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb
index b214486b3c1..d3ea8b955f2 100644
--- a/spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb
+++ b/spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb
@@ -8,6 +8,10 @@ RSpec.describe 'Project > Merge request > View user status' do
create(:merge_request, source_project: project, target_project: project, author: create(:user))
end
+ before do
+ stub_feature_flags(remove_user_attributes_projects: false)
+ end
+
subject { visit merge_request_path(merge_request) }
context 'for notes', :js do
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index b888e2f4171..f612956600f 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -4,39 +4,59 @@ require 'spec_helper'
RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
+ let_it_be(:user) { create(:user) }
- let(:user) { create(:user) }
- let(:access_level) { ProjectFeature::PUBLIC }
let(:role) { nil }
before do
project.add_role(user, role) if role
- project.project_feature.update_attribute(:operations_access_level, access_level)
-
sign_in(user)
- visit project_issues_path(project)
end
shared_examples 'shows Monitor menu based on the access level' do
- context 'when operations project feature is PRIVATE' do
- let(:access_level) { ProjectFeature::PRIVATE }
-
- it 'shows the `Monitor` menu' do
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
- end
+ using RSpec::Parameterized::TableSyntax
+
+ let(:enabled) { Featurable::PRIVATE }
+ let(:disabled) { Featurable::DISABLED }
+
+ where(:flag_enabled, :operations_access_level, :monitor_level, :render) do
+ true | ref(:disabled) | ref(:enabled) | true
+ true | ref(:disabled) | ref(:disabled) | false
+ true | ref(:enabled) | ref(:enabled) | true
+ true | ref(:enabled) | ref(:disabled) | false
+ false | ref(:disabled) | ref(:enabled) | false
+ false | ref(:disabled) | ref(:disabled) | false
+ false | ref(:enabled) | ref(:enabled) | true
+ false | ref(:enabled) | ref(:disabled) | true
end
- context 'when operations project feature is DISABLED' do
- let(:access_level) { ProjectFeature::DISABLED }
+ with_them do
+ it 'renders when expected to' do
+ stub_feature_flags(split_operations_visibility_permissions: flag_enabled)
+ project.project_feature.update_attribute(:operations_access_level, operations_access_level)
+ project.project_feature.update_attribute(:monitor_access_level, monitor_level)
+
+ visit project_issues_path(project)
- it 'does not show the `Monitor` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ if render
+ expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
+ else
+ expect(page).not_to have_selector('a.shortcuts-monitor')
+ end
end
end
end
- context 'user is not a member' do
+ context 'when user is not a member' do
+ let(:access_level) { ProjectFeature::PUBLIC }
+
+ before do
+ project.project_feature.update_attribute(:operations_access_level, access_level)
+ project.project_feature.update_attribute(:monitor_access_level, access_level)
+ end
+
it 'has the correct `Monitor` menu items', :aggregate_failures do
+ visit project_issues_path(project)
expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
expect(page).to have_link('Incidents', href: project_incidents_path(project))
expect(page).to have_link('Environments', href: project_environments_path(project))
@@ -48,27 +68,50 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
end
- context 'when operations project feature is PRIVATE' do
- let(:access_level) { ProjectFeature::PRIVATE }
+ context 'with new monitor visiblity flag disabled' do
+ stub_feature_flags(split_operations_visibility_permissions: false)
- it 'does not show the `Monitor` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ context 'when operations project feature is PRIVATE' do
+ let(:access_level) { ProjectFeature::PRIVATE }
+
+ it 'does not show the `Monitor` menu' do
+ expect(page).not_to have_selector('a.shortcuts-monitor')
+ end
+ end
+
+ context 'when operations project feature is DISABLED' do
+ let(:access_level) { ProjectFeature::DISABLED }
+
+ it 'does not show the `Operations` menu' do
+ expect(page).not_to have_selector('a.shortcuts-monitor')
+ end
end
end
- context 'when operations project feature is DISABLED' do
- let(:access_level) { ProjectFeature::DISABLED }
+ context 'with new monitor visiblity flag enabled' do
+ context 'when monitor project feature is PRIVATE' do
+ let(:access_level) { ProjectFeature::PRIVATE }
+
+ it 'does not show the `Monitor` menu' do
+ expect(page).not_to have_selector('a.shortcuts-monitor')
+ end
+ end
+
+ context 'when operations project feature is DISABLED' do
+ let(:access_level) { ProjectFeature::DISABLED }
- it 'does not show the `Operations` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ it 'does not show the `Operations` menu' do
+ expect(page).not_to have_selector('a.shortcuts-monitor')
+ end
end
end
end
- context 'user has guest role' do
+ context 'when user has guest role' do
let(:role) { :guest }
it 'has the correct `Monitor` menu items' do
+ visit project_issues_path(project)
expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
expect(page).to have_link('Incidents', href: project_incidents_path(project))
expect(page).to have_link('Environments', href: project_environments_path(project))
@@ -83,10 +126,11 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
it_behaves_like 'shows Monitor menu based on the access level'
end
- context 'user has reporter role' do
+ context 'when user has reporter role' do
let(:role) { :reporter }
it 'has the correct `Monitor` menu items' do
+ visit project_issues_path(project)
expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
expect(page).to have_link('Environments', href: project_environments_path(project))
@@ -100,10 +144,11 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
it_behaves_like 'shows Monitor menu based on the access level'
end
- context 'user has developer role' do
+ context 'when user has developer role' do
let(:role) { :developer }
it 'has the correct `Monitor` menu items' do
+ visit project_issues_path(project)
expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project))
expect(page).to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
@@ -116,10 +161,11 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
it_behaves_like 'shows Monitor menu based on the access level'
end
- context 'user has maintainer role' do
+ context 'when user has maintainer role' do
let(:role) { :maintainer }
it 'has the correct `Monitor` menu items' do
+ visit project_issues_path(project)
expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project))
expect(page).to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
diff --git a/spec/features/populate_new_pipeline_vars_with_params_spec.rb b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
index 744543d1252..75fa8561235 100644
--- a/spec/features/populate_new_pipeline_vars_with_params_spec.rb
+++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
@@ -7,24 +7,42 @@ RSpec.describe "Populate new pipeline CI variables with url params", :js do
let(:project) { create(:project) }
let(:page_path) { new_project_pipeline_path(project) }
- before do
- sign_in(user)
- project.add_maintainer(user)
+ shared_examples 'form pre-filled with URL params' do
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
- visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
+ visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
+ end
+
+ it "var[key1]=value1 populates env_var variable correctly" do
+ page.within(all("[data-testid='ci-variable-row']")[0]) do
+ expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1')
+ expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1')
+ end
+ end
+
+ it "file_var[key2]=value2 populates file variable correctly" do
+ page.within(all("[data-testid='ci-variable-row']")[1]) do
+ expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2')
+ expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2')
+ end
+ end
end
- it "var[key1]=value1 populates env_var variable correctly" do
- page.within(all("[data-testid='ci-variable-row']")[0]) do
- expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1')
- expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1')
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(run_pipeline_graphql: false)
end
+
+ it_behaves_like 'form pre-filled with URL params'
end
- it "file_var[key2]=value2 populates file variable correctly" do
- page.within(all("[data-testid='ci-variable-row']")[1]) do
- expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2')
- expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2')
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(run_pipeline_graphql: true)
end
+
+ it_behaves_like 'form pre-filled with URL params'
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 2836ac2f801..913c375f909 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -27,17 +27,41 @@ RSpec.describe 'Profile account page', :js do
expect(User.exists?(user.id)).to be_truthy
end
- it 'deletes user', :js, :sidekiq_might_not_need_inline do
- click_button 'Delete account'
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'deletes user', :js, :sidekiq_inline do
+ click_button 'Delete account'
- fill_in 'password', with: user.password
+ fill_in 'password', with: user.password
- page.within '.modal' do
- click_button 'Delete account'
+ page.within '.modal' do
+ click_button 'Delete account'
+ end
+
+ expect(page).to have_content('Account scheduled for removal')
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: user)
+ ).to be_exists
end
+ end
- expect(page).to have_content('Account scheduled for removal')
- expect(User.exists?(user.id)).to be_falsy
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'deletes user', :js, :sidekiq_inline do
+ click_button 'Delete account'
+
+ fill_in 'password', with: user.password
+
+ page.within '.modal' do
+ click_button 'Delete account'
+ end
+
+ expect(page).to have_content('Account scheduled for removal')
+ expect(User.exists?(user.id)).to be_falsy
+ end
end
it 'shows invalid password flash message', :js do
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index 24c9225532b..d0819bb5363 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
expect(page).to(
have_selector('ul.list-group li.list-group-item', text: 'Signed in on',
- count: 2))
+ count: 2))
expect(page).to have_content(
'127.0.0.1 ' \
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 2f7b722f553..d887a367fcb 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'User edit profile' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(remove_user_attributes_projects: false)
sign_in(user)
visit(profile_path)
end
@@ -179,7 +180,7 @@ RSpec.describe 'User edit profile' do
end
it 'adds emoji to user status' do
- emoji = 'biohazard'
+ emoji = 'basketball'
select_emoji(emoji)
submit_settings
@@ -192,7 +193,7 @@ RSpec.describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
- fill_in 'js-status-message-field', with: message
+ fill_in s_("SetStatusModal|What's your status?"), with: message
submit_settings
visit_user
@@ -207,7 +208,7 @@ RSpec.describe 'User edit profile' do
emoji = '8ball'
message = 'Playing outside'
select_emoji(emoji)
- fill_in 'js-status-message-field', with: message
+ fill_in s_("SetStatusModal|What's your status?"), with: message
submit_settings
visit_user
@@ -229,7 +230,7 @@ RSpec.describe 'User edit profile' do
end
visit(profile_path)
- click_button 'js-clear-user-status-button'
+ click_button s_('SetStatusModal|Clear status')
submit_settings
visit_user
@@ -239,9 +240,9 @@ RSpec.describe 'User edit profile' do
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
- fill_in 'js-status-message-field', with: message
+ fill_in s_("SetStatusModal|What's your status?"), with: message
- within('.js-toggle-emoji-menu') do
+ within('.emoji-menu-toggle-button') do
expect(page).to have_emoji('speech_balloon')
end
end
@@ -405,7 +406,7 @@ RSpec.describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
open_user_status_modal
- find('.js-status-message-field').native.send_keys(message)
+ find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal
visit_user
@@ -421,7 +422,7 @@ RSpec.describe 'User edit profile' do
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
- find('.js-status-message-field').native.send_keys(message)
+ find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal
visit_user
@@ -445,7 +446,7 @@ RSpec.describe 'User edit profile' do
open_edit_status_modal
- find('.js-clear-user-status-button').click
+ click_button s_('SetStatusModal|Clear status')
set_user_status_in_modal
visit_user
@@ -490,7 +491,7 @@ RSpec.describe 'User edit profile' do
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
open_user_status_modal
- find('.js-status-message-field').native.send_keys(message)
+ find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
expect(page).to have_emoji('speech_balloon')
end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 7dd2e6aafa3..df096c2f151 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -87,61 +87,4 @@ RSpec.describe 'User visits their profile' do
end
end
end
-
- describe 'storage_enforcement_banner', :js do
- before do
- stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
- end
-
- context 'with storage_enforcement_date set' do
- let_it_be(:storage_enforcement_date) { Date.today + 30 }
-
- before do
- allow_next_found_instance_of(Namespaces::UserNamespace) do |user_namespace|
- allow(user_namespace).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- end
-
- it 'displays the banner in the profile page' do
- visit(profile_path)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
-
- it 'does not display the banner if user has previously closed unless threshold has changed' do
- visit(profile_path)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- find('.js-storage-enforcement-banner [data-testid="close-icon"]').click
- page.refresh
- expect_page_not_to_have_storage_enforcement_banner
-
- storage_enforcement_date = Date.today + 13
- allow_next_found_instance_of(Namespaces::UserNamespace) do |user_namespace|
- allow(user_namespace).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- page.refresh
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
- end
-
- context 'with storage_enforcement_date not set' do
- before do
- allow_next_found_instance_of(Namespaces::UserNamespace) do |user_namespace|
- allow(user_namespace).to receive(:storage_enforcement_date).and_return(nil)
- end
- end
-
- it 'does not display the banner in the group page' do
- visit(profile_path)
- expect_page_not_to_have_storage_enforcement_banner
- end
- end
- end
-
- def expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- expect(page).to have_text "Effective #{storage_enforcement_date}, namespace storage limits will apply"
- end
-
- def expect_page_not_to_have_storage_enforcement_banner
- expect(page).not_to have_text "namespace storage limits will apply"
- end
end
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 89dbd1afc6b..d3bedbf3a75 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Project variables', :js do
project.variables << variable
end
- # TODO: Add same tests but with FF enabled context when
- # the new graphQL app for variable settings is enabled.
context 'with disabled ff `ci_variable_settings_graphql' do
before do
stub_feature_flags(ci_variable_settings_graphql: false)
@@ -44,4 +42,32 @@ RSpec.describe 'Project variables', :js do
end
end
end
+
+ context 'with enabled ff `ci_variable_settings_graphql' do
+ before do
+ visit page_path
+ end
+
+ it_behaves_like 'variable list'
+
+ it 'adds a new variable with an environment scope' do
+ click_button('Add variable')
+
+ page.within('#add-ci-variable') do
+ fill_in 'Key', with: 'akey'
+ find('#ci-variable-value').set('akey_value')
+ find('[data-testid="environment-scope"]').click
+ find('[data-testid="ci-environment-search"]').set('review/*')
+ find('[data-testid="create-wildcard-button"]').click
+
+ click_button('Add variable')
+ end
+
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 5c1bc1ad239..7555e567c37 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -191,7 +191,7 @@ RSpec.describe 'test coverage badge' do
def show_test_coverage_badge(job: nil, min_good: nil, min_acceptable: nil, min_medium: nil)
visit coverage_project_badges_path(project, ref: :master, job: job, min_good: min_good,
- min_acceptable: min_acceptable, min_medium: min_medium, format: :svg)
+ min_acceptable: min_acceptable, min_medium: min_medium, format: :svg)
end
def expect_coverage_badge(coverage)
diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb
index 3b2b74b469e..5287d5e4f7d 100644
--- a/spec/features/projects/blobs/blame_spec.rb
+++ b/spec/features/projects/blobs/blame_spec.rb
@@ -14,11 +14,32 @@ RSpec.describe 'File blame', :js do
wait_for_all_requests
end
+ context 'as a developer' do
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+
+ before do
+ project.add_role(user, role)
+ sign_in(user)
+ end
+
+ it 'does not display lock, replace and delete buttons' do
+ visit_blob_blame(path)
+
+ expect(page).not_to have_button("Lock")
+ expect(page).not_to have_button("Replace")
+ expect(page).not_to have_button("Delete")
+ end
+ end
+
it 'displays the blame page without pagination' do
visit_blob_blame(path)
- expect(page).to have_css('.blame-commit')
- expect(page).not_to have_css('.gl-pagination')
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).to have_css('.blame-commit')
+ expect(page).not_to have_css('.gl-pagination')
+ expect(page).not_to have_link _('View entire blame')
+ end
end
context 'when blob length is over the blame range limit' do
@@ -29,12 +50,15 @@ RSpec.describe 'File blame', :js do
it 'displays two first lines of the file with pagination' do
visit_blob_blame(path)
- expect(page).to have_css('.blame-commit')
- expect(page).to have_css('.gl-pagination')
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).to have_css('.blame-commit')
+ expect(page).to have_css('.gl-pagination')
+ expect(page).to have_link _('View entire blame')
- expect(page).to have_css('#L1')
- expect(page).not_to have_css('#L3')
- expect(find('.page-link.active')).to have_text('1')
+ expect(page).to have_css('#L1')
+ expect(page).not_to have_css('#L3')
+ expect(find('.page-link.active')).to have_text('1')
+ end
end
context 'when user clicks on the next button' do
@@ -45,15 +69,35 @@ RSpec.describe 'File blame', :js do
end
it 'displays next two lines of the file with pagination' do
- expect(page).not_to have_css('#L1')
- expect(page).to have_css('#L3')
- expect(find('.page-link.active')).to have_text('2')
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).not_to have_css('#L1')
+ expect(page).to have_css('#L3')
+ expect(find('.page-link.active')).to have_text('2')
+ end
end
it 'correctly redirects to the prior blame page' do
- find('.version-link').click
+ within '[data-testid="blob-content-holder"]' do
+ find('.version-link').click
+
+ expect(find('.page-link.active')).to have_text('2')
+ end
+ end
+ end
+
+ context 'when user clicks on View entire blame button' do
+ before do
+ visit_blob_blame(path)
+ end
+
+ it 'displays the blame page without pagination' do
+ within '[data-testid="blob-content-holder"]' do
+ click_link _('View entire blame')
- expect(find('.page-link.active')).to have_text('2')
+ expect(page).to have_css('#L1')
+ expect(page).to have_css('#L3')
+ expect(page).not_to have_css('.gl-pagination')
+ end
end
end
@@ -65,8 +109,11 @@ RSpec.describe 'File blame', :js do
it 'displays the blame page without pagination' do
visit_blob_blame(path)
- expect(page).to have_css('.blame-commit')
- expect(page).not_to have_css('.gl-pagination')
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).to have_css('.blame-commit')
+ expect(page).not_to have_css('.gl-pagination')
+ expect(page).not_to have_link _('View entire blame')
+ end
end
end
end
@@ -81,25 +128,29 @@ RSpec.describe 'File blame', :js do
it 'displays two hundred lines of the file with pagination' do
visit_blob_blame(path)
- expect(page).to have_css('.blame-commit')
- expect(page).to have_css('.gl-pagination')
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).to have_css('.blame-commit')
+ expect(page).to have_css('.gl-pagination')
- expect(page).to have_css('#L1')
- expect(page).not_to have_css('#L201')
- expect(find('.page-link.active')).to have_text('1')
+ expect(page).to have_css('#L1')
+ expect(page).not_to have_css('#L201')
+ expect(find('.page-link.active')).to have_text('1')
+ end
end
context 'when user clicks on the next button' do
before do
visit_blob_blame(path)
-
- find('.js-next-button').click
end
it 'displays next two hundred lines of the file with pagination' do
- expect(page).not_to have_css('#L1')
- expect(page).to have_css('#L201')
- expect(find('.page-link.active')).to have_text('2')
+ within '[data-testid="blob-content-holder"]' do
+ find('.js-next-button').click
+
+ expect(page).not_to have_css('#L1')
+ expect(page).to have_css('#L201')
+ expect(find('.page-link.active')).to have_text('2')
+ end
end
end
end
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
index 484f740faee..d2774aa74c9 100644
--- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -8,6 +8,10 @@ RSpec.describe 'User creates new blob', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :empty_repo) }
+ before do
+ stub_feature_flags(vscode_web_ide: false)
+ end
+
shared_examples 'creating a file' do
it 'allows the user to add a new file in Web IDE' do
visit project_path(project)
diff --git a/spec/features/projects/branches/user_creates_branch_spec.rb b/spec/features/projects/branches/user_creates_branch_spec.rb
index 18d083f7d88..be236b7ca7e 100644
--- a/spec/features/projects/branches/user_creates_branch_spec.rb
+++ b/spec/features/projects/branches/user_creates_branch_spec.rb
@@ -1,48 +1,129 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'spec_helper'
-RSpec.describe "User creates branch", :js do
+RSpec.describe 'User creates branch', :js do
include Spec::Support::Helpers::Features::BranchesHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:user) { create(:user) }
- before do
- project.add_developer(user)
- sign_in(user)
+ shared_examples 'creates new branch' do
+ specify do
+ branch_name = "deploy_keys_#{SecureRandom.hex(4)}"
- visit(new_project_branch_path(project))
+ create_branch(branch_name)
+
+ expect(page).to have_content(branch_name)
+ end
+ end
+
+ shared_examples 'renders not found page' do
+ specify do
+ expect(page).to have_title('Not Found')
+ expect(page).to have_content('Page Not Found')
+ end
end
- it "creates new branch" do
- branch_name = "deploy_keys"
+ context 'when project is public with private repository' do
+ let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- create_branch(branch_name)
+ context 'when user is an inherited member from the group' do
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
- expect(page).to have_content(branch_name)
- end
+ visit(new_project_branch_path(project))
+ end
- context "when branch name is invalid" do
- it "does not create new branch" do
- invalid_branch_name = "1.0 stable"
+ it_behaves_like 'renders not found page'
+ end
- fill_in("branch_name", with: invalid_branch_name)
- page.find("body").click # defocus the branch_name input
+ context 'and user is a developer' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
- select_branch("master")
- click_button("Create branch")
+ visit(new_project_branch_path(project))
+ end
- expect(page).to have_content("Branch name is invalid")
- expect(page).to have_content("can't contain spaces")
+ it_behaves_like 'creates new branch'
+ end
end
end
- context "when branch name already exists" do
- it "does not create new branch" do
- create_branch("master")
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'when user is a direct project member' do
+ context 'and user is a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(new_project_branch_path(project))
+ end
+
+ context 'when on new branch page' do
+ it 'renders I18n supported text' do
+ page.within('#new-branch-form') do
+ expect(page).to have_content(_('Branch name'))
+ expect(page).to have_content(_('Create from'))
+ expect(page).to have_content(_('Existing branch name, tag, or commit SHA'))
+ end
+ end
+ end
+
+ it_behaves_like 'creates new branch'
+
+ context 'when branch name is invalid' do
+ it 'does not create new branch' do
+ invalid_branch_name = '1.0 stable'
+
+ fill_in('branch_name', with: invalid_branch_name)
+ page.find('body').click # defocus the branch_name input
+
+ select_branch('master')
+ click_button('Create branch')
+
+ expect(page).to have_content('Branch name is invalid')
+ expect(page).to have_content("can't contain spaces")
+ end
+ end
+
+ context 'when branch name already exists' do
+ it 'does not create new branch' do
+ create_branch('master')
+
+ expect(page).to have_content('Branch already exists')
+ end
+ end
+ end
+ end
+
+ context 'when user is an inherited member from the group' do
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
+
+ visit(new_project_branch_path(project))
+ end
+
+ it_behaves_like 'renders not found page'
+ end
+
+ context 'and user is a developer' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+
+ visit(new_project_branch_path(project))
+ end
- expect(page).to have_content("Branch already exists")
+ it_behaves_like 'creates new branch'
+ end
end
end
end
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index e472cff38ce..417e14e2376 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
end
it 'displays a mini pipeline graph' do
- expect(page).to have_selector('[data-testid="commit-box-mini-graph"]')
+ expect(page).to have_selector('[data-testid="commit-box-pipeline-mini-graph"]')
first('[data-testid="mini-pipeline-graph-dropdown"]').click
@@ -35,7 +35,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
page.within '.js-builds-dropdown-list' do
expect(page).to have_selector('.ci-status-icon-running')
- expect(page).to have_content(build.stage)
+ expect(page).to have_content(build.stage_name)
end
build.drop
diff --git a/spec/features/projects/commits/multi_view_diff_spec.rb b/spec/features/projects/commits/multi_view_diff_spec.rb
index 5af2e367aed..c0e48b7b86c 100644
--- a/spec/features/projects/commits/multi_view_diff_spec.rb
+++ b/spec/features/projects/commits/multi_view_diff_spec.rb
@@ -11,8 +11,9 @@ RSpec.shared_examples "no multiple viewers" do |commit_ref|
end
RSpec.describe 'Multiple view Diffs', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+
let(:ref) { '5d6ed1503801ca9dc28e95eeb85a7cf863527aee' }
let(:path) { project_commit_path(project, ref) }
let(:feature_flag_on) { false }
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
index f5f4d13dd58..bf0949443de 100644
--- a/spec/features/projects/environments/environment_metrics_spec.rb
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -30,9 +30,9 @@ RSpec.describe 'Environment > Metrics' do
click_link 'Monitoring'
expect(page).to have_current_path(project_metrics_dashboard_path(project, environment: environment.id))
- expect(page).to have_css('[data-qa-selector="environments_dropdown"]') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_css('[data-testid="environments-dropdown"]')
- within('[data-qa-selector="environments_dropdown"]') do # rubocop:disable QA/SelectorUsage
+ within('[data-testid="environments-dropdown"]') do
# Click on the dropdown
click_on(environment.name)
@@ -59,7 +59,7 @@ RSpec.describe 'Environment > Metrics' do
visit_environment(environment)
click_link 'Monitoring'
- expect(page).to have_css('[data-qa-selector="prometheus_graphs"]') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_css('[data-testid="prometheus-graphs"]')
end
it_behaves_like 'has environment selector'
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index a53e8beb555..be4b21dfff4 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -264,9 +264,7 @@ RSpec.describe 'Environment' do
let(:build) { create(:ci_build, :success, pipeline: pipeline, environment: environment.name) }
let(:action) do
- create(:ci_build, :manual, pipeline: pipeline,
- name: 'close_app',
- environment: environment.name)
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app', environment: environment.name)
end
let(:deployment) do
@@ -278,8 +276,7 @@ RSpec.describe 'Environment' do
context 'when user has ability to stop environment' do
let(:permissions) do
- create(:protected_branch, :developers_can_merge,
- name: action.ref, project: project)
+ create(:protected_branch, :developers_can_merge, name: action.ref, project: project)
end
it 'allows to stop environment', :js do
diff --git a/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb b/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
index 221f07a2f75..949e530f86d 100644
--- a/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
+++ b/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
@@ -30,8 +30,7 @@ RSpec.describe 'User sees feature flag list', :js do
create(:operations_scope, strategy: strat, environment_scope: 'production')
end
end
- create(:operations_feature_flag, :new_version_flag, project: project,
- name: 'my_flag', active: false)
+ create(:operations_feature_flag, :new_version_flag, project: project, name: 'my_flag', active: false)
end
it 'shows the user the first flag' do
@@ -91,7 +90,7 @@ RSpec.describe 'User sees feature flag list', :js do
it 'shows the empty page' do
expect(page).to have_text 'Get started with feature flags'
expect(page).to have_selector('.btn-confirm', text: 'New feature flag')
- expect(page).to have_selector('[data-qa-selector="configure_feature_flags_button"]', text: 'Configure') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_selector('[data-testid="ff-configure-button"]', text: 'Configure')
end
end
end
diff --git a/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
index 71c9d89fbde..eb9ac078662 100644
--- a/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
+++ b/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
@@ -18,13 +18,12 @@ RSpec.describe 'User updates feature flag', :js do
context 'with a new version feature flag' do
let!(:feature_flag) do
- create_flag(project, 'test_flag', false, version: Operations::FeatureFlag.versions['new_version_flag'],
- description: 'For testing')
+ create_flag(project, 'test_flag', false,
+ version: Operations::FeatureFlag.versions['new_version_flag'], description: 'For testing')
end
let!(:strategy) do
- create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
end
let!(:scope) do
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 0ad44f31a52..52686469243 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license
let(:project_maintainer) { project.first_owner }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
sign_in(project_maintainer)
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index d7460538be9..1a9c5483218 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe 'Projects > Files > User edits files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
sign_in(user)
end
@@ -102,6 +104,21 @@ RSpec.describe 'Projects > Files > User edits files', :js do
expect(page).to have_content('*.rbca')
end
+ it 'shows loader on commit changes' do
+ set_default_button('edit')
+ click_link('.gitignore')
+ click_link_or_button('Edit')
+
+ # why: We don't want the form to actually submit, so that we can assert the button's changed state
+ page.execute_script("document.querySelector('.js-edit-blob-form').addEventListener('submit', e => e.preventDefault())")
+
+ find('.file-editor', match: :first)
+ editor_set_value('*.rbca')
+ click_button('Commit changes')
+
+ expect(page).to have_button('Commit changes', disabled: true, class: 'js-commit-button-loading')
+ end
+
it 'shows the diff of an edited file' do
set_default_button('edit')
click_link('.gitignore')
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index fb27f0961b6..b8c127f0078 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -126,7 +126,10 @@ RSpec.describe 'Project fork' do
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
def submit_form
- select(group.name)
+ find('[data-testid="select_namespace_dropdown"]').click
+ find('[data-testid="select_namespace_dropdown_search_field"]').fill_in(with: group.name)
+ click_button group.name
+
click_button 'Fork project'
end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 289ab8cffa5..995f4a1e3d2 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -58,8 +58,7 @@ RSpec.describe 'User browses jobs' do
context 'when a job can be canceled' do
let!(:job) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ create(:ci_build, pipeline: pipeline, stage: 'test')
end
before do
@@ -81,7 +80,7 @@ RSpec.describe 'User browses jobs' do
context 'when a job can be retried' do
let!(:job) do
create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ stage: 'test')
end
before do
@@ -190,7 +189,7 @@ RSpec.describe 'User browses jobs' do
context 'column links' do
let!(:job) do
create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ stage: 'test')
end
before do
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index 2ad820e4a06..c47350fb663 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Milestones sorting', :js do
let(:milestones_for_sort_by) do
{
'Due later' => %w[b c a],
- 'Name, ascending' => %w[a b c],
+ 'Name, ascending' => %w[a b c],
'Name, descending' => %w[c b a],
'Start later' => %w[a c b],
'Start soon' => %w[b c a],
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index e07a5d09405..5b5f7860e43 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'Project navbar' do
stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
- _('Packages & Registries'),
+ _('Packages and registries'),
within: _('Settings'),
new_sub_nav_item_name: _('Pages')
)
@@ -83,6 +83,8 @@ RSpec.describe 'Project navbar' do
end
context 'when harbor registry is available' do
+ let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
+
before do
stub_feature_flags(harbor_registry_integration: true)
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index f45025d079a..7cf05242a23 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -424,9 +424,10 @@ RSpec.describe 'New project', :js do
it 'keeps "Import project" tab open after form validation error' do
collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace)
- stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
- body: '001e# service=git-upload-pack',
- headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(
+ { status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
fill_in 'project_import_url', with: 'http://foo/bar'
fill_in 'project_name', with: collision_project.name
@@ -465,9 +466,10 @@ RSpec.describe 'New project', :js do
end
it 'initiates import when valid repo url is provided' do
- stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
- body: '001e# service=git-upload-pack',
- headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(
+ { status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
fill_in 'project_import_url', with: 'http://foo/bar'
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 0711a30e974..dcc46f5d223 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe 'Pipeline Schedules', :js do
it 'displays the required information description' do
page.within('.pipeline-schedule-table-row') do
expect(page).to have_content('pipeline schedule')
- expect(find(".next-run-cell time")['title'])
+ expect(find("[data-testid='next-run-cell'] time")['title'])
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
expect(page).to have_link('master')
expect(page).to have_link("##{pipeline.id}")
@@ -259,7 +259,7 @@ RSpec.describe 'Pipeline Schedules', :js do
click_button 'Save pipeline schedule'
page.within('.pipeline-schedule-table-row:nth-child(1)') do
- expect(page).to have_css(".next-run-cell time")
+ expect(page).to have_css("[data-testid='next-run-cell'] time")
end
end
end
diff --git a/spec/features/projects/pipelines/legacy_pipeline_spec.rb b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
index 14f60dfe061..250a336469c 100644
--- a/spec/features/projects/pipelines/legacy_pipeline_spec.rb
+++ b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
@@ -73,9 +73,9 @@ RSpec.describe 'Pipeline', :js do
visit_pipeline
expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('Build')
- expect(page).to have_content('Test')
- expect(page).to have_content('Deploy')
+ expect(page).to have_content('build')
+ expect(page).to have_content('test')
+ expect(page).to have_content('deploy')
expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
@@ -668,9 +668,9 @@ RSpec.describe 'Pipeline', :js do
it 'shows the pipeline graph' do
expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('Build')
- expect(page).to have_content('Test')
- expect(page).to have_content('Deploy')
+ expect(page).to have_content('build')
+ expect(page).to have_content('test')
+ expect(page).to have_content('deploy')
expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
@@ -769,13 +769,17 @@ RSpec.describe 'Pipeline', :js do
let(:resource_group) { create(:ci_resource_group, project: project) }
let!(:test_job) do
- create(:ci_build, :pending, stage: 'test', name: 'test',
- stage_idx: 1, pipeline: pipeline, project: project)
+ create(:ci_build, :pending, stage: 'test', name: 'test', stage_idx: 1, pipeline: pipeline, project: project)
end
let!(:deploy_job) do
- create(:ci_build, :created, stage: 'deploy', name: 'deploy',
- stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ create(:ci_build, :created,
+ stage: 'deploy',
+ name: 'deploy',
+ stage_idx: 2,
+ pipeline: pipeline,
+ project: project,
+ resource_group: resource_group)
end
describe 'GET /:project/-/pipelines/:id' do
@@ -873,8 +877,14 @@ RSpec.describe 'Pipeline', :js do
context 'when deploy job is a bridge to trigger a downstream pipeline' do
let!(:deploy_job) do
- create(:ci_bridge, :created, stage: 'deploy', name: 'deploy',
- stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ create(:ci_bridge, :created,
+ stage: 'deploy',
+ name: 'deploy',
+ stage_idx: 2,
+ pipeline: pipeline,
+ project: project,
+ resource_group: resource_group
+ )
end
it 'shows deploy job as waiting for resource' do
@@ -895,8 +905,14 @@ RSpec.describe 'Pipeline', :js do
context 'when deploy job is a bridge to trigger a downstream pipeline' do
let!(:deploy_job) do
- create(:ci_bridge, :created, stage: 'deploy', name: 'deploy',
- stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ create(:ci_bridge, :created,
+ stage: 'deploy',
+ name: 'deploy',
+ stage_idx: 2,
+ pipeline: pipeline,
+ project: project,
+ resource_group: resource_group
+ )
end
it 'shows deploy job as waiting for resource' do
@@ -1207,7 +1223,7 @@ RSpec.describe 'Pipeline', :js do
subject
expect(page).to have_content(failed_build.name)
- expect(page).to have_content(failed_build.stage)
+ expect(page).to have_content(failed_build.stage_name)
end
it 'shows build failure logs' do
@@ -1253,7 +1269,7 @@ RSpec.describe 'Pipeline', :js do
subject
expect(page).to have_content(failed_build.name)
- expect(page).to have_content(failed_build.stage)
+ expect(page).to have_content(failed_build.stage_name)
end
it 'does not show log' do
diff --git a/spec/features/projects/pipelines/legacy_pipelines_spec.rb b/spec/features/projects/pipelines/legacy_pipelines_spec.rb
index eb8f2de3aba..2e0ea695ab3 100644
--- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb
+++ b/spec/features/projects/pipelines/legacy_pipelines_spec.rb
@@ -546,8 +546,8 @@ RSpec.describe 'Pipelines', :js do
context 'for a failed pipeline' do
let!(:build) do
create(:ci_build, :failed, pipeline: pipeline,
- stage: 'build',
- name: 'build')
+ stage: 'build',
+ name: 'build')
end
it 'displays the failure reason' do
@@ -652,10 +652,10 @@ RSpec.describe 'Pipelines', :js do
expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
# stages
- expect(page).to have_text('Build')
- expect(page).to have_text('Test')
- expect(page).to have_text('Deploy')
- expect(page).to have_text('External')
+ expect(page).to have_text('build')
+ expect(page).to have_text('test')
+ expect(page).to have_text('deploy')
+ expect(page).to have_text('external')
# builds
expect(page).to have_text('rspec')
@@ -674,6 +674,7 @@ RSpec.describe 'Pipelines', :js do
let(:project) { create(:project, :repository) }
before do
+ stub_feature_flags(run_pipeline_graphql: false)
visit new_project_pipeline_path(project)
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cfdd851cb80..51a6fbc4d36 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -72,9 +72,9 @@ RSpec.describe 'Pipeline', :js do
visit_pipeline
expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('Build')
- expect(page).to have_content('Test')
- expect(page).to have_content('Deploy')
+ expect(page).to have_content('build')
+ expect(page).to have_content('test')
+ expect(page).to have_content('deploy')
expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
@@ -793,9 +793,9 @@ RSpec.describe 'Pipeline', :js do
it 'shows the pipeline graph' do
expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('Build')
- expect(page).to have_content('Test')
- expect(page).to have_content('Deploy')
+ expect(page).to have_content('build')
+ expect(page).to have_content('test')
+ expect(page).to have_content('deploy')
expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
@@ -895,12 +895,12 @@ RSpec.describe 'Pipeline', :js do
let!(:test_job) do
create(:ci_build, :pending, stage: 'test', name: 'test',
- stage_idx: 1, pipeline: pipeline, project: project)
+ stage_idx: 1, pipeline: pipeline, project: project)
end
let!(:deploy_job) do
create(:ci_build, :created, stage: 'deploy', name: 'deploy',
- stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
end
describe 'GET /:project/-/pipelines/:id' do
@@ -998,8 +998,14 @@ RSpec.describe 'Pipeline', :js do
context 'when deploy job is a bridge to trigger a downstream pipeline' do
let!(:deploy_job) do
- create(:ci_bridge, :created, stage: 'deploy', name: 'deploy',
- stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ create(:ci_bridge, :created,
+ stage: 'deploy',
+ name: 'deploy',
+ stage_idx: 2,
+ pipeline: pipeline,
+ project: project,
+ resource_group: resource_group
+ )
end
it 'shows deploy job as waiting for resource' do
@@ -1126,7 +1132,7 @@ RSpec.describe 'Pipeline', :js do
subject
expect(page).to have_content(failed_build.name)
- expect(page).to have_content(failed_build.stage)
+ expect(page).to have_content(failed_build.stage_name)
end
it 'shows build failure logs' do
@@ -1172,7 +1178,7 @@ RSpec.describe 'Pipeline', :js do
subject
expect(page).to have_content(failed_build.name)
- expect(page).to have_content(failed_build.stage)
+ expect(page).to have_content(failed_build.stage_name)
end
it 'does not show log' do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index bf521971ae0..404e51048bc 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -536,8 +536,8 @@ RSpec.describe 'Pipelines', :js do
context 'for a failed pipeline' do
let!(:build) do
create(:ci_build, :failed, pipeline: pipeline,
- stage: 'build',
- name: 'build')
+ stage: 'build',
+ name: 'build')
end
it 'displays the failure reason' do
@@ -635,10 +635,10 @@ RSpec.describe 'Pipelines', :js do
expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
# stages
- expect(page).to have_text('Build')
- expect(page).to have_text('Test')
- expect(page).to have_text('Deploy')
- expect(page).to have_text('External')
+ expect(page).to have_text('build')
+ expect(page).to have_text('test')
+ expect(page).to have_text('deploy')
+ expect(page).to have_text('external')
# builds
expect(page).to have_text('rspec')
@@ -656,19 +656,7 @@ RSpec.describe 'Pipelines', :js do
describe 'POST /:project/-/pipelines' do
let(:project) { create(:project, :repository) }
- before do
- visit new_project_pipeline_path(project)
- end
-
- context 'for valid commit', :js do
- before do
- click_button project.default_branch
- wait_for_requests
-
- find('p', text: 'master').click
- wait_for_requests
- end
-
+ shared_examples 'run pipeline form with gitlab-ci.yml' do
context 'with gitlab-ci.yml', :js do
before do
stub_ci_pipeline_to_return_yaml_file
@@ -702,7 +690,9 @@ RSpec.describe 'Pipelines', :js do
end
end
end
+ end
+ shared_examples 'run pipeline form without gitlab-ci.yml' do
context 'without gitlab-ci.yml' do
before do
click_on 'Run pipeline'
@@ -722,6 +712,51 @@ RSpec.describe 'Pipelines', :js do
end
end
end
+
+ # Run Pipeline form with REST endpoints
+ # TODO: Clean up tests when run_pipeline_graphql is enabled
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(run_pipeline_graphql: false)
+ visit new_project_pipeline_path(project)
+ end
+
+ context 'for valid commit', :js do
+ before do
+ click_button project.default_branch
+ wait_for_requests
+
+ find('p', text: 'master').click
+ wait_for_requests
+ end
+
+ it_behaves_like 'run pipeline form with gitlab-ci.yml'
+
+ it_behaves_like 'run pipeline form without gitlab-ci.yml'
+ end
+ end
+
+ # Run Pipeline form with GraphQL
+ context 'with feature flag enabled' do
+ before do
+ stub_feature_flags(run_pipeline_graphql: true)
+ visit new_project_pipeline_path(project)
+ end
+
+ context 'for valid commit', :js do
+ before do
+ click_button project.default_branch
+ wait_for_requests
+
+ find('p', text: 'master').click
+ wait_for_requests
+ end
+
+ it_behaves_like 'run pipeline form with gitlab-ci.yml'
+
+ it_behaves_like 'run pipeline form without gitlab-ci.yml'
+ end
+ end
end
describe 'Reset runner caches' do
diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb
index 10c4395da81..d82c4229b71 100644
--- a/spec/features/projects/releases/user_creates_release_spec.rb
+++ b/spec/features/projects/releases/user_creates_release_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'User creates release', :js do
fill_out_form_and_submit
end
- it 'creates a new release when "Create release" is clicked and redirects to the release\'s dedicated page', :aggregate_failures do
+ it 'creates a new release when "Create release" is clicked and redirects to the release\'s dedicated page', :aggregate_failures, :sidekiq_inline do
release = project.releases.last
expect(release.tag).to eq(tag_name)
@@ -66,6 +66,11 @@ RSpec.describe 'User creates release', :js do
expect(link.url).to eq(link_2[:url])
expect(link.name).to eq(link_2[:title])
+ expect(release).not_to be_historical_release
+ expect(release).not_to be_upcoming_release
+
+ expect(release.evidences.length).to eq(1)
+
expect(page).to have_current_path(project_release_path(project, release))
end
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
new file mode 100644
index 00000000000..ba84d8b6d1a
--- /dev/null
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Projects > Settings > Merge requests' do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, namespace: user.namespace, path: 'gitlab', name: 'sample') }
+
+ before do
+ sign_in(user)
+
+ visit(project_settings_merge_requests_path(project))
+ end
+
+ it 'shows "Merge commit" strategy' do
+ page.within '.merge-request-settings-form' do
+ expect(page).to have_content 'Merge commit'
+ end
+ end
+
+ it 'shows "Merge commit with semi-linear history " strategy' do
+ page.within '.merge-request-settings-form' do
+ expect(page).to have_content 'Merge commit with semi-linear history'
+ end
+ end
+
+ it 'shows "Fast-forward merge" strategy' do
+ page.within '.merge-request-settings-form' do
+ expect(page).to have_content 'Fast-forward merge'
+ end
+ end
+
+ it 'shows Squash commit options', :aggregate_failures do
+ page.within '.merge-request-settings-form' do
+ expect(page).to have_content 'Do not allow'
+ expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.'
+
+ expect(page).to have_content 'Allow'
+ expect(page).to have_content 'Checkbox is visible and unselected by default.'
+
+ expect(page).to have_content 'Encourage'
+ expect(page).to have_content 'Checkbox is visible and selected by default.'
+
+ expect(page).to have_content 'Require'
+ end
+ end
+
+ context 'when Merge Request and Pipelines are initially enabled', :js do
+ context 'when Pipelines are initially enabled' do
+ it 'shows the Merge Requests settings' do
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All threads must be resolved'
+
+ visit edit_project_path(project)
+
+ within('.sharing-permissions-form') do
+ within('[data-for="project[project_feature_attributes][merge_requests_access_level]"]') do
+ find('.gl-toggle').click
+ end
+ end
+
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
+
+ visit project_settings_merge_requests_path(project)
+
+ expect(page).to have_content('Not Found')
+ end
+ end
+
+ context 'when Pipelines are initially disabled', :js do
+ before do
+ project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
+
+ visit project_settings_merge_requests_path(project)
+ end
+
+ it 'shows the Merge Requests settings that do not depend on Builds feature' do
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All threads must be resolved'
+
+ visit edit_project_path(project)
+
+ within('.sharing-permissions-form') do
+ within('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"]') do
+ find('.gl-toggle').click
+ end
+ end
+
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
+
+ visit project_settings_merge_requests_path(project)
+
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All threads must be resolved'
+ end
+ end
+ end
+
+ context 'when Merge Request are initially disabled', :js do
+ before do
+ project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
+
+ visit(project_settings_merge_requests_path(project))
+ end
+
+ it 'does not show the Merge Requests settings' do
+ expect(page).to have_content('Not Found')
+
+ visit edit_project_path(project)
+
+ within('.sharing-permissions-form') do
+ within('[data-for="project[project_feature_attributes][merge_requests_access_level]"]') do
+ find('.gl-toggle').click
+ end
+ end
+
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
+
+ visit project_settings_merge_requests_path(project)
+
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All threads must be resolved'
+ end
+ end
+
+ describe 'Checkbox to enable merge request link', :js do
+ it 'is initially checked' do
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).to be_checked
+ end
+
+ it 'when unchecked sets :printing_merge_request_link_enabled to false' do
+ uncheck('project_printing_merge_request_link_enabled')
+ within('.merge-request-settings-form') do
+ find('.rspec-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ find('.flash-notice')
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+
+ expect(checkbox).not_to be_checked
+
+ project.reload
+ expect(project.printing_merge_request_link_enabled).to be(false)
+ end
+ end
+
+ describe 'Checkbox to remove source branch after merge', :js do
+ it 'is initially checked' do
+ checkbox = find_field('project_remove_source_branch_after_merge')
+ expect(checkbox).to be_checked
+ end
+
+ it 'when unchecked sets :remove_source_branch_after_merge to false' do
+ uncheck('project_remove_source_branch_after_merge')
+ within('.merge-request-settings-form') do
+ find('.rspec-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ find('.flash-notice')
+ checkbox = find_field('project_remove_source_branch_after_merge')
+
+ expect(checkbox).not_to be_checked
+
+ project.reload
+ expect(project.remove_source_branch_after_merge).to be(false)
+ end
+ end
+
+ describe 'Squash commits when merging', :js do
+ it 'initially has :squash_option set to :default_off' do
+ radio = find_field('project_project_setting_attributes_squash_option_default_off')
+ expect(radio).to be_checked
+ end
+
+ it 'allows :squash_option to be set to :default_on' do
+ choose('project_project_setting_attributes_squash_option_default_on')
+
+ within('.merge-request-settings-form') do
+ find('.rspec-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ wait_for_requests
+
+ radio = find_field('project_project_setting_attributes_squash_option_default_on')
+
+ expect(radio).to be_checked
+ expect(project.reload.project_setting.squash_option).to eq('default_on')
+ end
+
+ it 'allows :squash_option to be set to :always' do
+ choose('project_project_setting_attributes_squash_option_always')
+
+ within('.merge-request-settings-form') do
+ find('.rspec-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ wait_for_requests
+
+ radio = find_field('project_project_setting_attributes_squash_option_always')
+
+ expect(radio).to be_checked
+ expect(project.reload.project_setting.squash_option).to eq('always')
+ end
+
+ it 'allows :squash_option to be set to :never' do
+ choose('project_project_setting_attributes_squash_option_never')
+
+ within('.merge-request-settings-form') do
+ find('.rspec-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ wait_for_requests
+
+ radio = find_field('project_project_setting_attributes_squash_option_never')
+
+ expect(radio).to be_checked
+ expect(project.reload.project_setting.squash_option).to eq('never')
+ end
+ end
+
+ describe 'target project settings' do
+ context 'when project is a fork' do
+ let_it_be(:upstream) { create(:project, :public) }
+
+ let(:project) { fork_project(upstream, user) }
+
+ it 'allows to change merge request target project behavior' do
+ expect(page).to have_content 'The default target project for merge requests'
+
+ radio = find_field('project_project_setting_attributes_mr_default_target_self_false')
+ expect(radio).to be_checked
+
+ choose('project_project_setting_attributes_mr_default_target_self_true')
+
+ within('.merge-request-settings-form') do
+ find('.rspec-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ wait_for_requests
+
+ radio = find_field('project_project_setting_attributes_mr_default_target_self_true')
+
+ expect(radio).to be_checked
+ expect(project.reload.project_setting.mr_default_target_self).to be_truthy
+ end
+ end
+
+ it 'does not show target project section' do
+ expect(page).not_to have_content 'The default target project for merge requests'
+ end
+ end
+end
diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
index 5a50b3de772..477c4c2e1ba 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Packages & Registries > Container registry tag expiration policy' do
+RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
@@ -20,10 +20,89 @@ RSpec.describe 'Project > Settings > Packages & Registries > Container registry
end
context 'as owner', :js do
+ it 'shows active tab on sidebar' do
+ subject
+
+ expect(find('.sidebar-top-level-items > li.active')).to have_content('Settings')
+ expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
+ .to have_content('Packages and registries')
+ end
+
it 'shows available section' do
subject
expect(find('.breadcrumbs')).to have_content('Clean up image tags')
+
+ section = find('[data-testid="container-expiration-policy-project-settings"]')
+ expect(section).to have_text 'Clean up image tags'
+ end
+
+ it 'saves cleanup policy submit the form' do
+ subject
+
+ within '[data-testid="container-expiration-policy-project-settings"]' do
+ select('Every day', from: 'Run cleanup')
+ select('50 tags per image name', from: 'Keep the most recent:')
+ fill_in('Keep tags matching:', with: 'stable')
+ select('7 days', from: 'Remove tags older than:')
+ fill_in('Remove tags matching:', with: '.*-production')
+
+ submit_button = find('[data-testid="save-button"')
+ expect(submit_button).not_to be_disabled
+ submit_button.click
+ end
+
+ expect(page).to have_current_path(project_settings_packages_and_registries_path(project))
+ expect(find('.gl-alert-body')).to have_content('Cleanup policy successfully saved.')
+ end
+
+ it 'does not save cleanup policy submit form with invalid regex' do
+ subject
+
+ within '[data-testid="container-expiration-policy-project-settings"]' do
+ fill_in('Remove tags matching:', with: '*-production')
+
+ submit_button = find('[data-testid="save-button"')
+ expect(submit_button).not_to be_disabled
+ submit_button.click
+ end
+
+ expect(find('.gl-toast')).to have_content('Something went wrong while updating the cleanup policy.')
+ end
+ end
+
+ context 'with a project without expiration policy', :js do
+ before do
+ project.container_expiration_policy.destroy!
+ end
+
+ context 'with container_expiration_policies_enable_historic_entries enabled' do
+ before do
+ stub_application_setting(container_expiration_policies_enable_historic_entries: true)
+ end
+
+ it 'displays the related section' do
+ subject
+
+ within '[data-testid="container-expiration-policy-project-settings"]' do
+ expect(find('[data-testid="enable-toggle"]'))
+ .to have_content('Disabled - Tags will not be automatically deleted.')
+ end
+ end
+ end
+
+ context 'with container_expiration_policies_enable_historic_entries disabled' do
+ before do
+ stub_application_setting(container_expiration_policies_enable_historic_entries: false)
+ end
+
+ it 'does not display the related section' do
+ subject
+
+ within '[data-testid="container-expiration-policy-project-settings"]' do
+ expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 1fb46c669e7..d64570cd5cc 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Packages & Registries > Container registry tag expiration policy', :js do
+RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
@@ -19,48 +19,30 @@ RSpec.describe 'Project > Settings > Packages & Registries > Container registry
stub_container_registry_config(enabled: container_registry_enabled)
end
- context 'as owner' do
- it 'shows available section' do
+ context 'as owner', :js do
+ it 'shows active tab on sidebar' do
subject
- settings_block = find('[data-testid="container-expiration-policy-project-settings"]')
- expect(settings_block).to have_text 'Clean up image tags'
+ expect(find('.sidebar-top-level-items > li.active')).to have_content('Settings')
+ expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
+ .to have_content('Packages and registries')
end
- it 'saves cleanup policy submit the form' do
+ it 'shows available section' do
subject
- within '[data-testid="container-expiration-policy-project-settings"]' do
- select('Every day', from: 'Run cleanup')
- select('50 tags per image name', from: 'Keep the most recent:')
- fill_in('Keep tags matching:', with: 'stable')
- select('7 days', from: 'Remove tags older than:')
- fill_in('Remove tags matching:', with: '.*-production')
-
- submit_button = find('[data-testid="save-button"')
- expect(submit_button).not_to be_disabled
- submit_button.click
- end
-
- expect(find('.gl-toast')).to have_content('Cleanup policy successfully saved.')
+ settings_block = find('[data-testid="container-expiration-policy-project-settings"]')
+ expect(settings_block).to have_text 'Clean up image tags'
end
- it 'does not save cleanup policy submit form with invalid regex' do
+ it 'contains link to clean up image tags page' do
subject
- within '[data-testid="container-expiration-policy-project-settings"]' do
- fill_in('Remove tags matching:', with: '*-production')
-
- submit_button = find('[data-testid="save-button"')
- expect(submit_button).not_to be_disabled
- submit_button.click
- end
-
- expect(find('.gl-toast')).to have_content('Something went wrong while updating the cleanup policy.')
+ expect(page).to have_link('Edit cleanup rules', href: cleanup_image_tags_project_settings_packages_and_registries_path(project))
end
end
- context 'with a project without expiration policy' do
+ context 'with a project without expiration policy', :js do
before do
project.container_expiration_policy.destroy!
end
@@ -74,7 +56,7 @@ RSpec.describe 'Project > Settings > Packages & Registries > Container registry
subject
within '[data-testid="container-expiration-policy-project-settings"]' do
- expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
+ expect(page).to have_link('Set cleanup rules', href: cleanup_image_tags_project_settings_packages_and_registries_path(project))
end
end
end
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 6aa59f72d2a..c76b4d0af88 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -9,29 +9,29 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
before do
sign_in(user)
- visit edit_project_path(project)
+ visit project_settings_merge_requests_path(project)
end
it 'shows "Merge commit" strategy' do
- page.within '#js-merge-request-settings' do
+ page.within '.merge-request-settings-form' do
expect(page).to have_content 'Merge commit'
end
end
it 'shows "Merge commit with semi-linear history " strategy' do
- page.within '#js-merge-request-settings' do
+ page.within '.merge-request-settings-form' do
expect(page).to have_content 'Merge commit with semi-linear history'
end
end
it 'shows "Fast-forward merge" strategy' do
- page.within '#js-merge-request-settings' do
+ page.within '.merge-request-settings-form' do
expect(page).to have_content 'Fast-forward merge'
end
end
it 'shows Squash commit options', :aggregate_failures do
- page.within '#js-merge-request-settings' do
+ page.within '.merge-request-settings-form' do
expect(page).to have_content 'Do not allow'
expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.'
@@ -52,30 +52,33 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
- within('.sharing-permissions-form') do
- find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
- find('[data-testid="project-features-save-button"]').send_keys(:return)
- end
+ visit edit_project_path(project)
+
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
+
+ visit project_settings_merge_requests_path(project)
- expect(page).not_to have_content 'Pipelines must succeed'
- expect(page).not_to have_content 'All threads must be resolved'
+ expect(page).to have_content "Page Not Found"
end
end
context 'when Pipelines are initially disabled', :js do
before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
- visit edit_project_path(project)
+ visit project_settings_merge_requests_path(project)
end
it 'shows the Merge Requests settings that do not depend on Builds feature' do
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
- within('.sharing-permissions-form') do
- find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
- find('[data-testid="project-features-save-button"]').send_keys(:return)
- end
+ visit edit_project_path(project)
+
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
+
+ visit project_settings_merge_requests_path(project)
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
@@ -86,18 +89,22 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
context 'when Merge Request are initially disabled', :js do
before do
project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
- visit edit_project_path(project)
+ visit project_settings_merge_requests_path(project)
end
it 'does not show the Merge Requests settings' do
expect(page).not_to have_content 'Pipelines must succeed'
expect(page).not_to have_content 'All threads must be resolved'
+ visit edit_project_path(project)
+
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
+ visit project_settings_merge_requests_path(project)
+
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index fc78b5b5769..5cb12544066 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -28,26 +28,12 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
expect(visibility_select_container).to have_content 'Only accessible by project members. Membership must be explicitly granted to each user.'
end
- context 'merge requests select' do
- it 'hides merge requests section' do
- find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
-
- expect(page).to have_selector('.merge-requests-feature', visible: false)
- end
-
- context 'given project with merge_requests_disabled access level' do
- let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) }
-
- it 'hides merge requests section' do
- expect(page).to have_selector('.merge-requests-feature', visible: false)
- end
- end
- end
-
context 'builds select' do
it 'hides builds select section' do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
+ visit project_settings_merge_requests_path(project)
+
expect(page).to have_selector('.builds-feature', visible: false)
end
@@ -55,6 +41,8 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
let(:project) { create(:project, :builds_disabled, namespace: user.namespace) }
it 'hides builds select section' do
+ visit project_settings_merge_requests_path(project)
+
expect(page).to have_selector('.builds-feature', visible: false)
end
end
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index c84de7fc03f..d525544ac15 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -139,6 +139,12 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
expect(page).to have_current_path(edit_project_hook_path(project, hook), ignore_query: true)
end
+
+ it 'does not show search settings on the hook log details' do
+ visit project_hook_hook_log_path(project, hook, hook_log)
+
+ expect(page).not_to have_field(placeholder: 'Search settings', disabled: true)
+ end
end
end
end
diff --git a/spec/features/projects/show/user_interacts_with_stars_spec.rb b/spec/features/projects/show/user_interacts_with_stars_spec.rb
index aa61b629d92..e0dd4f65010 100644
--- a/spec/features/projects/show/user_interacts_with_stars_spec.rb
+++ b/spec/features/projects/show/user_interacts_with_stars_spec.rb
@@ -14,14 +14,36 @@ RSpec.describe 'Projects > Show > User interacts with project stars' do
end
it 'toggles the star' do
- find('.star-btn').click
+ star_project
expect(page).to have_css('.star-count', text: 1)
- find('.star-btn').click
+ unstar_project
expect(page).to have_css('.star-count', text: 0)
end
+
+ it 'validates starring a project' do
+ project.add_owner(user)
+
+ star_project
+
+ visit(dashboard_projects_path)
+
+ expect(page).to have_css('.stars', text: 1)
+ end
+
+ it 'validates un-starring a project' do
+ project.add_owner(user)
+
+ star_project
+
+ unstar_project
+
+ visit(dashboard_projects_path)
+
+ expect(page).to have_css('.stars', text: 0)
+ end
end
context 'when user is not signed in' do
@@ -38,3 +60,15 @@ RSpec.describe 'Projects > Show > User interacts with project stars' do
end
end
end
+
+private
+
+def star_project
+ click_button(_('Star'))
+ wait_for_requests
+end
+
+def unstar_project
+ click_button(_('Unstar'))
+ wait_for_requests
+end
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index fb2f0539558..1440db141a6 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
# The dropdown above the tree
page.within('.repo-breadcrumb') do
- find('.qa-add-to-tree').click # rubocop:disable QA/SelectorUsage
+ find('[data-testid="add-to-tree"]').click
aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New file')
@@ -71,7 +71,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
find_new_menu_toggle.click
end
- expect(page).not_to have_selector('.qa-add-to-tree') # rubocop:disable QA/SelectorUsage
+ expect(page).not_to have_selector('[data-testid="add-to-tree"]')
expect(page).not_to have_link('Web IDE')
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index 89f6b4237a4..5056e245fed 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -288,6 +288,17 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
end
end
+ it 'no Auto DevOps button if builds feature is disabled' do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+
+ visit project_path(project)
+
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Enable Auto DevOps')
+ expect(page).not_to have_link('Auto DevOps enabled')
+ end
+ end
+
it 'no "Enable Auto DevOps" button when .gitlab-ci.yml already exists' do
Files::CreateService.new(
project,
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 074469a9b55..9c950cfee6e 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe 'Multi-file editor new directory', :js do
let(:project) { create(:project, :repository) }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 85c644fa528..c0567ed4580 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe 'Multi-file editor new file', :js do
let(:project) { create(:project, :repository) }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 163e347d03d..eb0ef756b30 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('add tests for .gitattributes custom highlighting')
expect(page).not_to have_selector('[data-testid="alert-danger"]')
- expect(page).not_to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
+ expect(page).not_to have_selector('[data-testid="label-lfs"]', text: 'LFS')
end
it 'renders tree table for a subtree without errors' do
@@ -35,7 +35,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('add spaces in whitespace file')
- expect(page).not_to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
+ expect(page).not_to have_selector('[data-testid="label-lfs"]', text: 'LFS')
expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
@@ -112,11 +112,15 @@ RSpec.describe 'Projects tree', :js do
it 'renders LFS badge on blob item' do
visit project_tree_path(project, File.join('master', 'files/lfs'))
- expect(page).to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_selector('[data-testid="label-lfs"]', text: 'LFS')
end
end
context 'web IDE' do
+ before do
+ stub_feature_flags(vscode_web_ide: false)
+ end
+
it 'opens folder in IDE' do
visit project_tree_path(project, File.join('master', 'bar'))
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index ce00483bc91..f32141d6051 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Multi-file editor upload file', :js do
let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
before do
+ stub_feature_flags(vscode_web_ide: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index 7c970f7ee3d..b9b28398279 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -24,6 +24,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the group_canonical_path" do
+ stub_feature_flags(group_overview_tabs_vue: false)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@@ -32,6 +33,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the details_group_path" do
+ stub_feature_flags(group_overview_tabs_vue: false)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@@ -64,6 +66,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group homepage', :js do
before do
+ stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@@ -77,6 +80,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group details', :js do
before do
+ stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index d228fb084c3..cbd9340b737 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -418,8 +418,7 @@ RSpec.describe 'Project' do
visit path
end
- it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' },
- { form: '.rspec-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }]
+ it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }]
end
describe 'view for a user without an access to a repo' do
@@ -440,103 +439,6 @@ RSpec.describe 'Project' do
end
end
- describe 'storage_enforcement_banner', :js do
- let_it_be(:group) { create(:group) }
- let_it_be_with_refind(:user) { create(:user) }
- let_it_be(:project) { create(:project, group: group) }
-
- before do
- group.add_maintainer(user)
- sign_in(user)
- end
-
- context 'with storage_enforcement_date set' do
- let_it_be(:storage_enforcement_date) { Date.today + 30 }
-
- before do
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- end
-
- it 'displays the banner in the project page' do
- visit project_path(project)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
-
- context 'when in a subgroup project page' do
- let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:project) { create(:project, namespace: subgroup) }
-
- it 'displays the banner' do
- visit project_path(project)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
- end
-
- context 'when in a user namespace project page' do
- let_it_be(:project) { create(:project, namespace: user.namespace) }
-
- before do
- allow_next_found_instance_of(Namespaces::UserNamespace) do |user_namespace|
- allow(user_namespace).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- end
-
- it 'displays the banner' do
- visit project_path(project)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
- end
-
- it 'does not display the banner in a paid group project page' do
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:paid?).and_return(true)
- end
- visit project_path(project)
- expect_page_not_to_have_storage_enforcement_banner
- end
-
- it 'does not display the banner if user has previously closed unless threshold has changed' do
- visit project_path(project)
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- find('.js-storage-enforcement-banner [data-testid="close-icon"]').click
- wait_for_requests
- page.refresh
- expect_page_not_to_have_storage_enforcement_banner
-
- storage_enforcement_date = Date.today + 13
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
- page.refresh
- expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- end
- end
-
- context 'with storage_enforcement_date not set' do
- before do
- allow_next_found_instance_of(Group) do |group|
- allow(group).to receive(:storage_enforcement_date).and_return(nil)
- end
- end
-
- it 'does not display the banner in the group page' do
- stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
- visit project_path(project)
- expect_page_not_to_have_storage_enforcement_banner
- end
- end
- end
-
- def expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
- expect(page).to have_text "Effective #{storage_enforcement_date}, namespace storage limits will apply"
- end
-
- def expect_page_not_to_have_storage_enforcement_banner
- expect(page).not_to have_text "namespace storage limits will apply"
- end
-
def remove_with_confirm(button_text, confirm_with, confirm_button_text = 'Confirm')
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 2600c00346e..482f3d62f36 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Runners' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
@@ -24,25 +24,25 @@ RSpec.describe 'Runners' do
end
context 'when a project has enabled shared_runners' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
before do
project.add_maintainer(user)
end
context 'when a project_type runner is activated on the project' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
it 'user sees the specific runner' do
visit project_runners_path(project)
within '.activated-specific-runners' do
- expect(page).to have_content(specific_runner.display_name)
+ expect(page).to have_content(project_runner.display_name)
end
- click_on specific_runner.short_sha
+ click_on project_runner.short_sha
- expect(page).to have_content(specific_runner.platform)
+ expect(page).to have_content(project_runner.platform)
end
it 'user can pause and resume the specific runner' do
@@ -72,7 +72,7 @@ RSpec.describe 'Runners' do
click_on 'Remove runner'
end
- expect(page).not_to have_content(specific_runner.display_name)
+ expect(page).not_to have_content(project_runner.display_name)
end
it 'user edits the runner to be protected' do
@@ -92,7 +92,7 @@ RSpec.describe 'Runners' do
context 'when a runner has a tag' do
before do
- specific_runner.update!(tag_list: ['tag'])
+ project_runner.update!(tag_list: ['tag'])
end
it 'user edits runner not to run untagged jobs' do
@@ -120,24 +120,23 @@ RSpec.describe 'Runners' do
expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
end
end
- end
- context 'when multiple runners are configured' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
- let!(:specific_runner_2) { create(:ci_runner, :project, projects: [project]) }
+ context 'when multiple runners are configured' do
+ let!(:project_runner_2) { create(:ci_runner, :project, projects: [project]) }
- it 'adds pagination to the runner list' do
- stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
+ it 'adds pagination to the runner list' do
+ stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
- visit project_runners_path(project)
+ visit project_runners_path(project)
- expect(find('.pagination')).not_to be_nil
+ expect(find('.pagination')).not_to be_nil
+ end
end
end
context 'when a specific runner exists in another project' do
let(:another_project) { create(:project) }
- let!(:specific_runner) { create(:ci_runner, :project, projects: [another_project]) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [another_project]) }
before do
another_project.add_maintainer(user)
@@ -150,13 +149,13 @@ RSpec.describe 'Runners' do
click_on 'Enable for this project'
end
- expect(page.find('.activated-specific-runners')).to have_content(specific_runner.display_name)
+ expect(page.find('.activated-specific-runners')).to have_content(project_runner.display_name)
within '.activated-specific-runners' do
click_on 'Disable for this project'
end
- expect(page.find('.available-specific-runners')).to have_content(specific_runner.display_name)
+ expect(page.find('.available-specific-runners')).to have_content(project_runner.display_name)
end
end
@@ -255,7 +254,8 @@ RSpec.describe 'Runners' do
project.add_maintainer(user)
end
- let(:group) { create :group }
+ let_it_be(:group) { create :group }
+ let_it_be(:project) { create :project, group: group }
context 'as project and group maintainer' do
before do
@@ -263,8 +263,6 @@ RSpec.describe 'Runners' do
end
context 'project with a group but no group runner' do
- let(:project) { create :project, group: group }
-
it 'group runners are not available' do
visit project_runners_path(project)
@@ -280,8 +278,6 @@ RSpec.describe 'Runners' do
end
context 'project with a group but no group runner' do
- let(:project) { create :project, group: group }
-
it 'group runners are available' do
visit project_runners_path(project)
@@ -304,44 +300,46 @@ RSpec.describe 'Runners' do
end
end
- context 'project with a group but no group runner' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
+ context 'with group project' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
- it 'group runners are not available' do
- visit project_runners_path(project)
+ context 'project with a group but no group runner' do
+ it 'group runners are not available' do
+ visit project_runners_path(project)
- expect(page).to have_content 'This group does not have any group runners yet.'
+ expect(page).to have_content 'This group does not have any group runners yet.'
- expect(page).not_to have_content 'To register them, go to the group\'s Runners page.'
- expect(page).to have_content 'Ask your group owner to set up a group runner.'
+ expect(page).not_to have_content 'To register them, go to the group\'s Runners page.'
+ expect(page).to have_content 'Ask your group owner to set up a group runner.'
+ end
end
- end
- context 'project with a group and a group runner' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let!(:ci_runner) { create(:ci_runner, :group, groups: [group], description: 'group-runner') }
+ context 'project with a group and a group runner' do
+ let_it_be(:ci_runner) do
+ create(:ci_runner, :group, groups: [group], description: 'group-runner')
+ end
- it 'group runners are available' do
- visit project_runners_path(project)
+ it 'group runners are available' do
+ visit project_runners_path(project)
- expect(page).to have_content 'Available group runners: 1'
- expect(page).to have_content 'group-runner'
- end
+ expect(page).to have_content 'Available group runners: 1'
+ expect(page).to have_content 'group-runner'
+ end
- it 'group runners may be disabled for a project' do
- visit project_runners_path(project)
+ it 'group runners may be disabled for a project' do
+ visit project_runners_path(project)
- click_on 'Disable group runners'
+ click_on 'Disable group runners'
- expect(page).to have_content 'Enable group runners'
- expect(project.reload.group_runners_enabled).to be false
+ expect(page).to have_content 'Enable group runners'
+ expect(project.reload.group_runners_enabled).to be false
- click_on 'Enable group runners'
+ click_on 'Enable group runners'
- expect(page).to have_content 'Disable group runners'
- expect(project.reload.group_runners_enabled).to be true
+ expect(page).to have_content 'Disable group runners'
+ expect(project.reload.group_runners_enabled).to be true
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 53c95b4a446..e2c8708be78 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -69,8 +69,12 @@ RSpec.describe 'User searches for code' do
expect(page).to have_selector('.results', text: expected_result)
- find('.js-project-refs-dropdown').click
- find('.dropdown-page-one .dropdown-content').click_link('v1.0.0')
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ find('li', text: 'v1.0.0').click
+ end
expect(page).to have_selector('.results', text: expected_result)
@@ -96,36 +100,41 @@ RSpec.describe 'User searches for code' do
end
it 'shows ref switcher in code result summary' do
- expect(find('.js-project-refs-dropdown')).to have_text(ref_name)
+ expect(find('.ref-selector')).to have_text(ref_name)
end
it 'persists branch name across search' do
find('.gl-search-box-by-click-search-button').click
- expect(find('.js-project-refs-dropdown')).to have_text(ref_name)
+ expect(find('.ref-selector')).to have_text(ref_name)
end
# this example is use to test the desgine that the refs is not
# only repersent the branch as well as the tags.
it 'ref swither list all the branchs and tags' do
- find('.js-project-refs-dropdown').click
- expect(find('.dropdown-page-one .dropdown-content')).to have_link('sha-starting-with-large-number')
- expect(find('.dropdown-page-one .dropdown-content')).to have_link('v1.0.0')
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
end
it 'search result changes when refs switched' do
+ ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack')
- find('.js-project-refs-dropdown').click
- find('.dropdown-page-one .dropdown-content').click_link('master')
+ find('.ref-selector').click
+ wait_for_requests
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
- end
+ page.within('.ref-selector') do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
+
+ find('li', text: ref).click
+ end
- it 'persist refs over browser tabs' do
- ref = 'feature'
- find('.js-project-refs-dropdown').click
- link = find_link(ref)[:href]
- expect(link.include?("repository_ref=" + ref)).to be(true)
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
end
end
@@ -146,36 +155,41 @@ RSpec.describe 'User searches for code' do
end
it 'shows ref switcher in code result summary' do
- expect(find('.js-project-refs-dropdown')).to have_text(ref_name)
+ expect(find('.ref-selector')).to have_text(ref_name)
end
it 'persists branch name across search' do
find('.gl-search-box-by-click-search-button').click
- expect(find('.js-project-refs-dropdown')).to have_text(ref_name)
+ expect(find('.ref-selector')).to have_text(ref_name)
end
# this example is use to test the desgine that the refs is not
# only repersent the branch as well as the tags.
it 'ref swither list all the branchs and tags' do
- find('.js-project-refs-dropdown').click
- expect(find('.dropdown-page-one .dropdown-content')).to have_link('sha-starting-with-large-number')
- expect(find('.dropdown-page-one .dropdown-content')).to have_link('v1.0.0')
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
end
it 'search result changes when refs switched' do
+ ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack')
- find('.js-project-refs-dropdown').click
- find('.dropdown-page-one .dropdown-content').click_link('master')
+ find('.ref-selector').click
+ wait_for_requests
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
- end
+ page.within('.ref-selector') do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
+
+ find('li', text: ref).click
+ end
- it 'persist refs over browser tabs' do
- ref = 'feature'
- find('.js-project-refs-dropdown').click
- link = find_link(ref)[:href]
- expect(link.include?("repository_ref=" + ref)).to be(true)
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
end
end
@@ -187,12 +201,12 @@ RSpec.describe 'User searches for code' do
submit_search('test')
select_search_scope('Code')
- expect(page).to have_selector('.js-project-refs-dropdown')
+ expect(page).to have_selector('.ref-selector')
select_search_scope('Issues')
expect(find(:css, '.results')).to have_link(issue.title)
- expect(page).not_to have_selector('.js-project-refs-dropdown')
+ expect(page).not_to have_selector('.ref-selector')
end
end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 1523586ab26..41288a34fb2 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'User uses header search field', :js do
end
before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
sign_in(user)
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 628468a2abe..fd95516090a 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe 'User creates snippet', :js do
context 'when snippets default visibility level is restricted' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
- default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
+ default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
end
it 'creates a snippet using the lowest available visibility level as default' do
diff --git a/spec/features/tags/developer_creates_tag_spec.rb b/spec/features/tags/developer_creates_tag_spec.rb
index b0219cb546d..ca76a94092e 100644
--- a/spec/features/tags/developer_creates_tag_spec.rb
+++ b/spec/features/tags/developer_creates_tag_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'Developer creates tag' do
it 'opens dropdown for ref', :js do
click_link 'New tag'
- ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
+ ref_row = find('.form-group:nth-of-type(2) .col-sm-12')
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
diff --git a/spec/features/user_opens_link_to_comment_spec.rb b/spec/features/user_opens_link_to_comment_spec.rb
index ae84f69f432..3fb1505ff5b 100644
--- a/spec/features/user_opens_link_to_comment_spec.rb
+++ b/spec/features/user_opens_link_to_comment_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'User opens link to comment', :js do
wait_for_requests
- expect(page.find('#discussion-filter-dropdown')).to have_content('Show all activity')
+ expect(find('#discussion-preferences-dropdown')).to have_content('Sort or filter')
expect(page).not_to have_content('Something went wrong while fetching comments')
# Auto-switching to show all notes shouldn't be persisted
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index c8301c2fc91..f7102eaf9b7 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
# Expect an error message
expect_log_message('Failed Attempt', reason: 'rate_limited')
expect(page).to have_content("You've reached the maximum amount of tries. "\
- 'Wait 10 minutes or resend a new code and try again.')
+ 'Wait 10 minutes or send a new code and try again.')
# Wait for 10 minutes
travel 10.minutes
@@ -138,7 +138,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
# Expect an error message
expect_log_message('Failed Attempt', reason: 'invalid')
- expect(page).to have_content('The code is incorrect. Enter it again, or resend a new code.')
+ expect(page).to have_content('The code is incorrect. Enter it again, or send a new code.')
end
it 'verifies expired codes' do
@@ -150,12 +150,12 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
code = expect_instructions_email_and_extract_code
# Wait for the code to expire before verifying
- travel VerifiesWithEmail::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
+ travel Users::EmailVerification::ValidateTokenService::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
verify_code(code)
# Expect an error message
expect_log_message('Failed Attempt', reason: 'expired')
- expect(page).to have_content('The code has expired. Resend a new code and try again.')
+ expect(page).to have_content('The code has expired. Send a new code and try again.')
end
end
end
@@ -255,7 +255,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
perform_enqueued_jobs do
# The user is prompted for a verification code
gitlab_sign_in(user)
- expect(page).to have_content('Help us protect your account')
+ expect(page).to have_content(s_('IdentityVerification|Help us protect your account'))
code = expect_instructions_email_and_extract_code
# We toggle the feature flag off
@@ -266,12 +266,13 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
new_code = expect_instructions_email_and_extract_code
verify_code(code)
- expect(page).to have_content('The code is incorrect. Enter it again, or resend a new code.')
+ expect(page)
+ .to have_content(s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.'))
- travel VerifiesWithEmail::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
+ travel Users::EmailVerification::ValidateTokenService::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
verify_code(new_code)
- expect(page).to have_content('The code has expired. Resend a new code and try again.')
+ expect(page).to have_content(s_('IdentityVerification|The code has expired. Send a new code and try again.'))
click_link 'Resend code'
another_code = expect_instructions_email_and_extract_code
@@ -296,7 +297,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
it 'the unlock link still works' do
# The user is locked and unlock instructions are sent
- expect(page).to have_content('Invalid login or password.')
+ expect(page).to have_content(_('Invalid login or password.'))
user.reload
expect(user.locked_at).not_to be_nil
expect(user.unlock_token).not_to be_nil
@@ -334,24 +335,24 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
def expect_instructions_email_and_extract_code
mail = find_email_for(user)
expect(mail.to).to match_array([user.email])
- expect(mail.subject).to eq('Verify your identity')
- code = mail.body.parts.first.to_s[/\d{#{VerifiesWithEmail::TOKEN_LENGTH}}/o]
+ expect(mail.subject).to eq(s_('IdentityVerification|Verify your identity'))
+ code = mail.body.parts.first.to_s[/\d{#{Users::EmailVerification::GenerateTokenService::TOKEN_LENGTH}}/o]
reset_delivered_emails!
code
end
def verify_code(code)
- fill_in 'Verification code', with: code
- click_button 'Verify code'
+ fill_in s_('IdentityVerification|Verification code'), with: code
+ click_button s_('IdentityVerification|Verify code')
end
def expect_log_message(event = nil, times = 1, reason: '', message: nil)
expect(Gitlab::AppLogger).to have_received(:info)
.exactly(times).times
.with(message || hash_including(message: 'Email Verification',
- event: event,
- username: user.username,
- ip: '127.0.0.1',
- reason: reason))
+ event: event,
+ username: user.username,
+ ip: '127.0.0.1',
+ reason: reason))
end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index b875dbe1340..5ca5bd72b79 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -105,6 +105,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
before do
stub_application_setting(send_user_confirmation_email: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
+ stub_feature_flags(identity_verification: false)
end
context 'within the grace period' do
@@ -862,7 +863,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
context 'when the user already enabled 2FA' do
before do
user.update!(otp_required_for_login: true,
- otp_secret: User.generate_otp_secret(32))
+ otp_secret: User.generate_otp_secret(32))
end
it 'asks the user to accept the terms' do
@@ -954,6 +955,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
before do
stub_application_setting(send_user_confirmation_email: true)
stub_feature_flags(soft_email_confirmation: true)
+ stub_feature_flags(identity_verification: false)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 068e1fd4243..bbf5882f89f 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -339,7 +339,7 @@ RSpec.describe 'User page' do
subject
- page.within '.navbar-nav' do
+ page.within '.navbar-gitlab' do
expect(page).to have_link('Sign in')
end
end
@@ -351,7 +351,7 @@ RSpec.describe 'User page' do
subject
- page.within '.navbar-nav' do
+ page.within '.navbar-gitlab' do
expect(page).to have_link('Sign in / Register')
end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index f2381e41de8..de53e722603 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -66,6 +66,7 @@ RSpec.describe 'Signup' do
flag_values = [true, false]
flag_values.each do |val|
before do
+ stub_feature_flags(arkose_labs_signup_challenge: false)
stub_feature_flags(restyle_login_page: val)
stub_application_setting(require_admin_approval_after_user_signup: false)
end
@@ -202,6 +203,7 @@ RSpec.describe 'Signup' do
context 'when soft email confirmation is not enabled' do
before do
stub_feature_flags(soft_email_confirmation: false)
+ stub_feature_flags(identity_verification: false)
end
it 'creates the user account and sends a confirmation email, and pre-fills email address after confirming' do
@@ -297,9 +299,8 @@ RSpec.describe 'Signup' do
enforce_terms
end
- it 'renders text that the user confirms terms by clicking register' do
+ it 'renders text that the user confirms terms by signing in' do
visit new_user_registration_path
-
expect(page).to have_content(/By clicking Register, I agree that I have read and accepted the Terms of Use and Privacy Policy/)
fill_in_signup_form
@@ -391,7 +392,7 @@ RSpec.describe 'Signup' do
enforce_terms
end
- it 'renders text that the user confirms terms by clicking register' do
+ it 'renders text that the user confirms terms by signing in' do
visit new_user_registration_path
expect(page).to have_content(/By clicking Register, I agree that I have read and accepted the Terms of Use and Privacy Policy/)
diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb
new file mode 100644
index 00000000000..95774680a2b
--- /dev/null
+++ b/spec/features/work_items/work_item_children_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Work item children', :js do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, namespace: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ context 'for signed in user' do
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+
+ stub_feature_flags(work_items: true)
+ stub_feature_flags(work_items_hierarchy: true)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+ end
+
+ it 'are not displayed when issue does not have work item children', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ expect(find('[data-testid="links-empty"]')).to have_content(_('No tasks are currently assigned.'))
+ expect(page).not_to have_selector('[data-testid="add-links-form"]')
+ expect(page).not_to have_selector('[data-testid="links-child"]')
+ end
+ end
+
+ it 'toggles widget body', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ expect(page).to have_selector('[data-testid="links-body"]')
+
+ click_button 'Collapse tasks'
+
+ expect(page).not_to have_selector('[data-testid="links-body"]')
+
+ click_button 'Expand tasks'
+
+ expect(page).to have_selector('[data-testid="links-body"]')
+ end
+ end
+
+ it 'toggles form', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ expect(page).not_to have_selector('[data-testid="add-links-form"]')
+
+ click_button 'Add'
+
+ expect(page).to have_selector('[data-testid="add-links-form"]')
+
+ click_button 'Cancel'
+
+ expect(page).not_to have_selector('[data-testid="add-links-form"]')
+ end
+ end
+
+ it 'addss a child task', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ click_button 'Add'
+
+ expect(page).to have_button('Create task', disabled: true)
+ fill_in 'Add a title', with: 'Task 1'
+
+ expect(page).to have_button('Create task', disabled: false)
+
+ click_button 'Create task'
+
+ wait_for_all_requests
+
+ expect(find('[data-testid="links-child"]')).to have_content('Task 1')
+ end
+ end
+
+ it 'removes a child task and undoing', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ click_button 'Add'
+ fill_in 'Add a title', with: 'Task 1'
+ click_button 'Create task'
+ wait_for_all_requests
+
+ expect(find('[data-testid="links-child"]')).to have_content('Task 1')
+ expect(find('[data-testid="children-count"]')).to have_content('1')
+
+ find('[data-testid="links-menu"]').click
+ click_button 'Remove'
+
+ wait_for_all_requests
+
+ expect(page).not_to have_content('Task 1')
+ expect(find('[data-testid="children-count"]')).to have_content('0')
+ end
+
+ page.within('.gl-toast') do
+ expect(find('.toast-body')).to have_content(_('Child removed'))
+ find('.b-toaster a', text: 'Undo').click
+ end
+
+ wait_for_all_requests
+
+ page.within('[data-testid="work-item-links"]') do
+ expect(find('[data-testid="links-child"]')).to have_content('Task 1')
+ expect(find('[data-testid="children-count"]')).to have_content('1')
+ end
+ end
+ end
+end
diff --git a/spec/finders/bulk_imports/entities_finder_spec.rb b/spec/finders/bulk_imports/entities_finder_spec.rb
index 54c792cb4d8..9e8d04400eb 100644
--- a/spec/finders/bulk_imports/entities_finder_spec.rb
+++ b/spec/finders/bulk_imports/entities_finder_spec.rb
@@ -88,10 +88,11 @@ RSpec.describe BulkImports::EntitiesFinder do
let(:order) { :asc }
it 'returns entities sorted ascending' do
- expect(subject.execute).to eq([
- started_entity_1, finished_entity_1, failed_entity_1,
- started_entity_2, finished_entity_2, failed_entity_2
- ])
+ expect(subject.execute).to eq(
+ [
+ started_entity_1, finished_entity_1, failed_entity_1,
+ started_entity_2, finished_entity_2, failed_entity_2
+ ])
end
end
@@ -99,10 +100,11 @@ RSpec.describe BulkImports::EntitiesFinder do
let(:order) { :desc }
it 'returns entities sorted descending' do
- expect(subject.execute).to eq([
- failed_entity_2, finished_entity_2, started_entity_2,
- failed_entity_1, finished_entity_1, started_entity_1
- ])
+ expect(subject.execute).to eq(
+ [
+ failed_entity_2, finished_entity_2, started_entity_2,
+ failed_entity_1, finished_entity_1, started_entity_1
+ ])
end
end
end
diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
index 5352cfe5238..add941089b0 100644
--- a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
+++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
@@ -50,12 +50,13 @@ RSpec.describe Ci::DailyBuildGroupReportResultsFinder do
let(:current_user) { user_with_permission }
it 'returns matching coverages within the given date range' do
- expect(coverages).to match_array([
- karma_coverage_2,
- rspec_coverage_2,
- karma_coverage_1,
- rspec_coverage_1
- ])
+ expect(coverages).to match_array(
+ [
+ karma_coverage_2,
+ rspec_coverage_2,
+ karma_coverage_1,
+ rspec_coverage_1
+ ])
end
context 'when ref_path is nil' do
@@ -73,10 +74,11 @@ RSpec.describe Ci::DailyBuildGroupReportResultsFinder do
let(:limit) { 2 }
it 'returns limited number of matching coverages within the given date range' do
- expect(coverages).to match_array([
- karma_coverage_2,
- rspec_coverage_2
- ])
+ expect(coverages).to match_array(
+ [
+ karma_coverage_2,
+ rspec_coverage_2
+ ])
end
end
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index 45e8cf5a582..dd3ba9721e4 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Ci::JobsFinder, '#execute' do
context 'scope is an array' do
let(:jobs) { [pending_job, running_job, successful_job, canceled_job] }
- let(:params) {{ scope: %w'running success' }}
+ let(:params) { { scope: %w'running success' } }
it 'filters by the job statuses in the scope' do
expect(subject).to contain_exactly(running_job, successful_job)
diff --git a/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb b/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb
index a7cf041f553..6e218db1254 100644
--- a/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb
+++ b/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
let!(:pipeline) do
create(:ci_empty_pipeline, project: project,
- sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+ sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
end
it 'returns pipelines from diff_head_sha' do
@@ -140,7 +140,7 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
let!(:branch_pipeline) do
create(:ci_pipeline, source: :push, project: project,
- ref: source_ref, sha: merge_request.merge_request_diff.head_commit_sha)
+ ref: source_ref, sha: merge_request.merge_request_diff.head_commit_sha)
end
let!(:tag_pipeline) do
@@ -149,12 +149,12 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
let!(:detached_merge_request_pipeline) do
create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: source_ref, sha: shas.second, merge_request: merge_request)
+ ref: source_ref, sha: shas.second, merge_request: merge_request)
end
let(:merge_request) do
create(:merge_request, source_project: project, source_branch: source_ref,
- target_project: project, target_branch: target_ref)
+ target_project: project, target_branch: target_ref)
end
let(:project) { create(:project, :repository) }
@@ -167,12 +167,12 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
context 'when there are a branch pipeline and a merge request pipeline' do
let!(:branch_pipeline_2) do
create(:ci_pipeline, source: :push, project: project,
- ref: source_ref, sha: shas.first)
+ ref: source_ref, sha: shas.first)
end
let!(:detached_merge_request_pipeline_2) do
create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: source_ref, sha: shas.first, merge_request: merge_request)
+ ref: source_ref, sha: shas.first, merge_request: merge_request)
end
it 'returns merge request pipelines first' do
@@ -184,7 +184,7 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
context 'when there are multiple merge request pipelines from the same branch' do
let!(:branch_pipeline_2) do
create(:ci_pipeline, source: :push, project: project,
- ref: source_ref, sha: shas.first)
+ ref: source_ref, sha: shas.first)
end
let!(:branch_pipeline_with_sha_not_belonging_to_merge_request) do
@@ -193,12 +193,12 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
let!(:detached_merge_request_pipeline_2) do
create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: source_ref, sha: shas.first, merge_request: merge_request_2)
+ ref: source_ref, sha: shas.first, merge_request: merge_request_2)
end
let(:merge_request_2) do
create(:merge_request, source_project: project, source_branch: source_ref,
- target_project: project, target_branch: 'stable')
+ target_project: project, target_branch: 'stable')
end
before do
@@ -220,7 +220,7 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
context 'when detached merge request pipeline is run on head ref of the merge request' do
let!(:detached_merge_request_pipeline) do
create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: merge_request.ref_path, sha: shas.second, merge_request: merge_request)
+ ref: merge_request.ref_path, sha: shas.second, merge_request: merge_request)
end
it 'sets the head ref of the merge request to the pipeline ref' do
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 96412c1e371..8d3c375385a 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -260,13 +260,13 @@ RSpec.describe Ci::RunnersFinder do
let_it_be(:runner_sub_group_2) { create(:ci_runner, :group, contacted_at: 10.minutes.ago) }
let_it_be(:runner_sub_group_3) { create(:ci_runner, :group, contacted_at: 9.minutes.ago) }
let_it_be(:runner_sub_group_4) { create(:ci_runner, :group, contacted_at: 8.minutes.ago) }
- let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project])}
- let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 6.minutes.ago, projects: [project_2])}
- let_it_be(:runner_project_3) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [project, project_2])}
- let_it_be(:runner_project_4) { create(:ci_runner, :project, contacted_at: 4.minutes.ago, projects: [project_3])}
- let_it_be(:runner_project_5) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project_4])}
- let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5])}
- let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6])}
+ let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project]) }
+ let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 6.minutes.ago, projects: [project_2]) }
+ let_it_be(:runner_project_3) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [project, project_2]) }
+ let_it_be(:runner_project_4) { create(:ci_runner, :project, contacted_at: 4.minutes.ago, projects: [project_3]) }
+ let_it_be(:runner_project_5) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project_4]) }
+ let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5]) }
+ let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6]) }
let(:target_group) { nil }
let(:membership) { nil }
diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb
index e8648d131ff..94bcec6163e 100644
--- a/spec/finders/concerns/packages/finder_helper_spec.rb
+++ b/spec/finders/concerns/packages/finder_helper_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ::Packages::FinderHelper do
subject { finder.execute(project1) }
- it { is_expected.to eq [package1]}
+ it { is_expected.to eq [package1] }
end
describe '#packages_visible_to_user' do
@@ -61,7 +61,7 @@ RSpec.describe ::Packages::FinderHelper do
end
shared_examples 'returning package1' do
- it { is_expected.to eq [package1]}
+ it { is_expected.to eq [package1] }
end
shared_examples 'returning no packages' do
@@ -165,7 +165,7 @@ RSpec.describe ::Packages::FinderHelper do
end
shared_examples 'returning project1' do
- it { is_expected.to eq [project1]}
+ it { is_expected.to eq [project1] }
end
shared_examples 'returning no project' do
diff --git a/spec/finders/container_repositories_finder_spec.rb b/spec/finders/container_repositories_finder_spec.rb
index 5d449d1b811..472c39d1f23 100644
--- a/spec/finders/container_repositories_finder_spec.rb
+++ b/spec/finders/container_repositories_finder_spec.rb
@@ -28,9 +28,9 @@ RSpec.describe ContainerRepositoriesFinder do
context "with name set to #{name}" do
let(:params) { { name: name } }
- it { is_expected.to contain_exactly(project_repository)}
+ it { is_expected.to contain_exactly(project_repository) }
- it { is_expected.not_to include(not_searched_repository)}
+ it { is_expected.not_to include(not_searched_repository) }
end
end
end
@@ -50,7 +50,7 @@ RSpec.describe ContainerRepositoriesFinder do
context "with sort set to #{order}" do
let(:params) { { sort: order } }
- it { is_expected.to eq([sort_repository2, sort_repository])}
+ it { is_expected.to eq([sort_repository2, sort_repository]) }
end
end
@@ -58,7 +58,7 @@ RSpec.describe ContainerRepositoriesFinder do
context "with sort set to #{order}" do
let(:params) { { sort: order } }
- it { is_expected.to eq([sort_repository, sort_repository2])}
+ it { is_expected.to eq([sort_repository, sort_repository2]) }
end
end
end
diff --git a/spec/finders/context_commits_finder_spec.rb b/spec/finders/context_commits_finder_spec.rb
index 95c685aea24..c22675bc67d 100644
--- a/spec/finders/context_commits_finder_spec.rb
+++ b/spec/finders/context_commits_finder_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ContextCommitsFinder do
describe "#execute" do
let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request) }
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
let(:commit) { create(:commit, id: '6d394385cf567f80a8fd85055db1ab4c5295806f') }
it 'filters commits by valid sha/commit message' do
@@ -24,5 +24,29 @@ RSpec.describe ContextCommitsFinder do
expect(commits).to be_empty
end
+
+ it 'returns commits based in author filter' do
+ params = { search: 'test text', author: 'Job van der Voort' }
+ commits = described_class.new(project, merge_request, params).execute
+
+ expect(commits.length).to eq(1)
+ expect(commits[0].id).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0')
+ end
+
+ it 'returns commits based in before filter' do
+ params = { search: 'test text', committed_before: 1474828200 }
+ commits = described_class.new(project, merge_request, params).execute
+
+ expect(commits.length).to eq(1)
+ expect(commits[0].id).to eq('498214de67004b1da3d820901307bed2a68a8ef6')
+ end
+
+ it 'returns commits based in after filter' do
+ params = { search: 'test text', committed_after: 1474828200 }
+ commits = described_class.new(project, merge_request, params).execute
+
+ expect(commits.length).to eq(1)
+ expect(commits[0].id).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0')
+ end
end
end
diff --git a/spec/finders/crm/organizations_finder_spec.rb b/spec/finders/crm/organizations_finder_spec.rb
index f227fcd3748..c89ac3b1cb5 100644
--- a/spec/finders/crm/organizations_finder_spec.rb
+++ b/spec/finders/crm/organizations_finder_spec.rb
@@ -128,5 +128,76 @@ RSpec.describe Crm::OrganizationsFinder do
end
end
end
+
+ context 'when sorting' do
+ let_it_be(:group) { create(:group, :crm_enabled) }
+
+ let_it_be(:sort_test_a) do
+ create(
+ :organization,
+ group: group,
+ name: "ABC",
+ description: "1"
+ )
+ end
+
+ let_it_be(:sort_test_b) do
+ create(
+ :organization,
+ group: group,
+ name: "DEF",
+ description: "2",
+ default_rate: 10
+ )
+ end
+
+ let_it_be(:sort_test_c) do
+ create(
+ :organization,
+ group: group,
+ name: "GHI",
+ default_rate: 20
+ )
+ end
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns the organiztions sorted by name in ascending order' do
+ finder = described_class.new(user, group: group, sort: { field: 'name', direction: :asc })
+
+ expect(finder.execute).to eq([sort_test_a, sort_test_b, sort_test_c])
+ end
+
+ it 'returns the organizations sorted by description in descending order' do
+ finder = described_class.new(user, group: group, sort: { field: 'description', direction: :desc })
+
+ expect(finder.execute).to eq([sort_test_b, sort_test_a, sort_test_c])
+ end
+
+ it 'returns the contacts sorted by default_rate in ascending order' do
+ finder = described_class.new(user, group: group, sort: { field: 'default_rate', direction: :asc })
+
+ expect(finder.execute).to eq([sort_test_b, sort_test_c, sort_test_a])
+ end
+ end
+ end
+
+ describe '.counts_by_state' do
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:active_organizations) { create_list(:organization, 3, group: group, state: :active) }
+ let_it_be(:inactive_organizations) { create_list(:organization, 2, group: group, state: :inactive) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns correct counts' do
+ counts = described_class.counts_by_state(user, group: group)
+
+ expect(counts["active"]).to eq(3)
+ expect(counts["inactive"]).to eq(2)
+ end
end
end
diff --git a/spec/finders/database/batched_background_migrations_finder_spec.rb b/spec/finders/database/batched_background_migrations_finder_spec.rb
new file mode 100644
index 00000000000..bd88be72fa5
--- /dev/null
+++ b/spec/finders/database/batched_background_migrations_finder_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Database::BatchedBackgroundMigrationsFinder do
+ let!(:migration_1) { create(:batched_background_migration, created_at: Time.now - 2) }
+ let!(:migration_2) { create(:batched_background_migration, created_at: Time.now - 1) }
+ let!(:migration_3) { create(:batched_background_migration, created_at: Time.now - 3) }
+
+ let(:finder) { described_class.new(connection: connection) }
+
+ describe '#execute' do
+ let(:connection) { ApplicationRecord.connection }
+
+ subject { finder.execute }
+
+ it 'returns migrations order by created_at (DESC)' do
+ is_expected.to eq([migration_2, migration_1, migration_3])
+ end
+
+ it 'limits the number of returned migrations' do
+ stub_const('Database::BatchedBackgroundMigrationsFinder::RETURNED_MIGRATIONS', 2)
+
+ is_expected.to eq([migration_2, migration_1])
+ end
+ end
+end
diff --git a/spec/finders/deploy_tokens/tokens_finder_spec.rb b/spec/finders/deploy_tokens/tokens_finder_spec.rb
index 7f19c5bf11b..4c72a2ced7c 100644
--- a/spec/finders/deploy_tokens/tokens_finder_spec.rb
+++ b/spec/finders/deploy_tokens/tokens_finder_spec.rb
@@ -30,24 +30,26 @@ RSpec.describe DeployTokens::TokensFinder do
it 'returns all deploy tokens' do
expect(subject.size).to eq(6)
- is_expected.to match_array([
- project_deploy_token,
- revoked_project_deploy_token,
- expired_project_deploy_token,
- group_deploy_token,
- revoked_group_deploy_token,
- expired_group_deploy_token
- ])
+ is_expected.to match_array(
+ [
+ project_deploy_token,
+ revoked_project_deploy_token,
+ expired_project_deploy_token,
+ group_deploy_token,
+ revoked_group_deploy_token,
+ expired_group_deploy_token
+ ])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens' do
- is_expected.to match_array([
- project_deploy_token,
- group_deploy_token
- ])
+ is_expected.to match_array(
+ [
+ project_deploy_token,
+ group_deploy_token
+ ])
end
end
@@ -68,11 +70,12 @@ RSpec.describe DeployTokens::TokensFinder do
end
it 'returns all deploy tokens for the project' do
- is_expected.to match_array([
- project_deploy_token,
- revoked_project_deploy_token,
- expired_project_deploy_token
- ])
+ is_expected.to match_array(
+ [
+ project_deploy_token,
+ revoked_project_deploy_token,
+ expired_project_deploy_token
+ ])
end
context 'and active filter is applied' do
@@ -100,11 +103,12 @@ RSpec.describe DeployTokens::TokensFinder do
end
it 'returns all deploy tokens for the group' do
- is_expected.to match_array([
- group_deploy_token,
- revoked_group_deploy_token,
- expired_group_deploy_token
- ])
+ is_expected.to match_array(
+ [
+ group_deploy_token,
+ revoked_group_deploy_token,
+ expired_group_deploy_token
+ ])
end
context 'and active filter is applied' do
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 51c293bcfd1..efb739c3d2f 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -32,7 +32,17 @@ RSpec.describe DeploymentsFinder do
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
- '`finished_at` filter and `finished_at` sorting must be paired')
+ '`finished_at` filter requires `finished_at` sort.')
+ end
+ end
+
+ context 'when running status filter and finished_at sorting' do
+ let(:params) { { status: :running, order_by: :finished_at } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ described_class::InefficientQueryError,
+ '`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.')
end
end
@@ -52,7 +62,17 @@ RSpec.describe DeploymentsFinder do
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
- '`environment` filter must be combined with `project` scope.')
+ '`environment` name filter must be combined with `project` scope.')
+ end
+ end
+
+ context 'when status filter with mixed finished and upcoming statuses' do
+ let(:params) { { status: [:success, :running] } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ described_class::InefficientQueryError,
+ 'finished statuses and upcoming statuses must be separately queried.')
end
end
end
@@ -103,6 +123,24 @@ RSpec.describe DeploymentsFinder do
end
end
+ context 'when the environment ID is specified' do
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:deployment1) do
+ create(:deployment, project: project, environment: environment1)
+ end
+
+ let!(:deployment2) do
+ create(:deployment, project: project, environment: environment2)
+ end
+
+ let(:params) { { environment: environment1.id } }
+
+ it 'returns deployments for the given environment' do
+ is_expected.to match_array([deployment1])
+ end
+ end
+
context 'when the deployment status is specified' do
let!(:deployment1) { create(:deployment, :success, project: project) }
let!(:deployment2) { create(:deployment, :failed, project: project) }
diff --git a/spec/finders/design_management/versions_finder_spec.rb b/spec/finders/design_management/versions_finder_spec.rb
index 0d606ef46f1..7dafdcfda97 100644
--- a/spec/finders/design_management/versions_finder_spec.rb
+++ b/spec/finders/design_management/versions_finder_spec.rb
@@ -71,13 +71,13 @@ RSpec.describe DesignManagement::VersionsFinder do
describe 'returning versions earlier or equal to a version' do
context 'when argument is the first version' do
- let(:params) { { earlier_or_equal_to: version_1 }}
+ let(:params) { { earlier_or_equal_to: version_1 } }
it { is_expected.to eq([version_1]) }
end
context 'when argument is the second version' do
- let(:params) { { earlier_or_equal_to: version_2 }}
+ let(:params) { { earlier_or_equal_to: version_2 } }
it { is_expected.to contain_exactly(version_1, version_2) }
end
diff --git a/spec/finders/environments/environments_finder_spec.rb b/spec/finders/environments/environments_finder_spec.rb
index 71d10ceb5d3..04fbd4067b4 100644
--- a/spec/finders/environments/environments_finder_spec.rb
+++ b/spec/finders/environments/environments_finder_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Environments::EnvironmentsFinder do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.creator }
let_it_be(:environment) { create(:environment, :available, project: project) }
- let_it_be(:environment_stopped) { create(:environment, :stopped, name: 'test2', project: project) }
- let_it_be(:environment_available) { create(:environment, :available, name: 'test3', project: project) }
+ let_it_be(:environment_stopped) { create(:environment, :stopped, name: 'test/test2', project: project) }
+ let_it_be(:environment_available) { create(:environment, :available, name: 'test/test3', project: project) }
before do
project.add_maintainer(user)
@@ -65,5 +65,11 @@ RSpec.describe Environments::EnvironmentsFinder do
expect(result).to contain_exactly(environment_available)
end
end
+
+ it 'filters environments by type' do
+ result = described_class.new(project, user, type: 'test').execute
+
+ expect(result).to contain_exactly(environment_stopped, environment_available)
+ end
end
end
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 5c5db874e85..2a9e887450c 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -131,7 +131,7 @@ RSpec.describe GroupDescendantsFinder do
project = create(:project, namespace: group)
other_project = create(:project)
other_project.project_group_links.create!(group: group,
- group_access: Gitlab::Access::MAINTAINER)
+ group_access: Gitlab::Access::MAINTAINER)
expect(finder.execute).to contain_exactly(project)
end
@@ -231,8 +231,8 @@ RSpec.describe GroupDescendantsFinder do
other_subgroup.add_developer(other_user)
finder = described_class.new(current_user: other_user,
- parent_group: group,
- params: params)
+ parent_group: group,
+ params: params)
expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 00aa14209a2..0d1b58e2636 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -21,10 +21,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
let(:groups) do
{
- group: group,
- sub_group: sub_group,
- sub_sub_group: sub_sub_group,
- public_shared_group: public_shared_group,
+ group: group,
+ sub_group: sub_group,
+ sub_sub_group: sub_sub_group,
+ public_shared_group: public_shared_group,
private_shared_group: private_shared_group
}
end
@@ -32,26 +32,27 @@ RSpec.describe GroupMembersFinder, '#execute' do
context 'relations' do
let!(:members) do
{
- user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
- user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
- user1_group: create(:group_member, :reporter, group: group, user: user1),
- user1_public_shared_group: create(:group_member, :maintainer, group: public_shared_group, user: user1),
+ user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
+ user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
+ user1_group: create(:group_member, :reporter, group: group, user: user1),
+ user1_public_shared_group: create(:group_member, :maintainer, group: public_shared_group, user: user1),
user1_private_shared_group: create(:group_member, :maintainer, group: private_shared_group, user: user1),
- user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2),
- user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2),
- user2_group: create(:group_member, :maintainer, group: group, user: user2),
- user2_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user2),
- user2_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user2),
- user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now),
- user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now),
- user3_group: create(:group_member, :reporter, group: group, user: user3),
- user3_public_shared_group: create(:group_member, :reporter, group: public_shared_group, user: user3),
- user3_private_shared_group: create(:group_member, :reporter, group: private_shared_group, user: user3),
- user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4),
- user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now),
- user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now),
- user4_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user4),
- user4_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user4)
+ user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2),
+ user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2),
+ user2_group: create(:group_member, :maintainer, group: group, user: user2),
+ user2_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user2),
+ user2_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user2),
+ user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now),
+ user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now),
+ user3_group: create(:group_member, :reporter, group: group, user: user3),
+ user3_public_shared_group: create(:group_member, :reporter, group: public_shared_group, user: user3),
+ user3_private_shared_group: create(:group_member, :reporter, group: private_shared_group, user: user3),
+ user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4),
+ user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now),
+ user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now),
+ user4_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user4),
+ user4_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user4),
+ user5_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user5)
}
end
@@ -76,15 +77,15 @@ RSpec.describe GroupMembersFinder, '#execute' do
[:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
[:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
- [:shared_from_groups] | :sub_group | []
- [:direct, :inherited, :descendants, :shared_from_groups] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:shared_from_groups] | :sub_group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
+ [:direct, :inherited, :descendants, :shared_from_groups] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
[] | :sub_sub_group | []
GroupMembersFinder::DEFAULT_RELATIONS | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:descendants] | :sub_sub_group | []
- [:shared_from_groups] | :sub_sub_group | []
- [:direct, :inherited, :descendants, :shared_from_groups] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:shared_from_groups] | :sub_sub_group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
+ [:direct, :inherited, :descendants, :shared_from_groups] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
end
with_them do
diff --git a/spec/finders/groups/accepting_group_transfers_finder_spec.rb b/spec/finders/groups/accepting_group_transfers_finder_spec.rb
new file mode 100644
index 00000000000..1a6c6f9243b
--- /dev/null
+++ b/spec/finders/groups/accepting_group_transfers_finder_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::AcceptingGroupTransfersFinder do
+ let_it_be(:current_user) { create(:user) }
+
+ let_it_be(:great_grandparent_group) do
+ create(:group, name: 'great grandparent group', path: 'great-grandparent-group')
+ end
+
+ let_it_be(:grandparent_group) { create(:group, parent: great_grandparent_group) }
+ let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
+ let_it_be(:child_group) { create(:group, parent: parent_group) }
+ let_it_be(:grandchild_group) { create(:group, parent: child_group) }
+ let_it_be(:group_where_user_has_owner_access) do
+ create(:group, name: 'owner access group', path: 'owner-access-group').tap do |group|
+ group.add_owner(current_user)
+ end
+ end
+
+ let_it_be(:subgroup_of_group_where_user_has_owner_access) do
+ create(:group, parent: group_where_user_has_owner_access)
+ end
+
+ let_it_be(:group_where_user_has_developer_access) do
+ create(:group).tap do |group|
+ group.add_developer(current_user)
+ end
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_guest) { create(:group) }
+ let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) }
+ let_it_be(:subgroup_of_shared_with_group_where_direct_owner_as_owner) do
+ create(:group, parent: shared_with_group_where_direct_owner_as_owner)
+ end
+
+ let(:params) { {} }
+
+ describe '#execute' do
+ before_all do
+ create(:group_group_link, :owner,
+ shared_with_group: group_where_user_has_owner_access,
+ shared_group: shared_with_group_where_direct_owner_as_owner
+ )
+
+ create(:group_group_link, :guest,
+ shared_with_group: group_where_user_has_owner_access,
+ shared_group: shared_with_group_where_direct_owner_as_guest
+ )
+ end
+
+ let(:group_to_be_transferred) { parent_group }
+
+ subject(:result) do
+ described_class.new(current_user, group_to_be_transferred, params).execute
+ end
+
+ context 'when the user does not have the rights to transfer the group' do
+ before do
+ group_to_be_transferred.root_ancestor.add_developer(current_user)
+ end
+
+ it 'returns empty result' do
+ expect(result).to be_empty
+ end
+ end
+
+ context 'when the user has the rights to transfer the group' do
+ before do
+ group_to_be_transferred.root_ancestor.add_owner(current_user)
+ end
+
+ it 'does not return empty result' do
+ expect(result).not_to be_empty
+ end
+
+ it 'excludes the descendants of the group to be transferred' do
+ expect(result).not_to include(child_group, grandchild_group)
+ end
+
+ it 'excludes the immediate parent of the group to be transferred' do
+ expect(result).not_to include(grandparent_group)
+ end
+
+ it 'excludes the groups where the user does not have OWNER access' do
+ expect(result).not_to include(group_where_user_has_developer_access)
+ end
+
+ it 'excludes the groups arising from group shares where the user does not have OWNER access' do
+ expect(result).not_to include(shared_with_group_where_direct_owner_as_guest)
+ end
+
+ it 'includes ancestors, except immediate parent of the group to be transferred' do
+ expect(result).to include(great_grandparent_group)
+ end
+
+ it 'includes the other groups where the user has OWNER access' do
+ expect(result).to include(group_where_user_has_owner_access)
+ end
+
+ it 'includes the other groups where the user has OWNER access through inherited membership' do
+ expect(result).to include(subgroup_of_group_where_user_has_owner_access)
+ end
+
+ it 'includes the groups where the user has OWNER access through group shares' do
+ expect(result).to include(
+ shared_with_group_where_direct_owner_as_owner,
+ subgroup_of_shared_with_group_where_direct_owner_as_owner
+ )
+ end
+
+ context 'on searching with a specific term' do
+ let(:params) { { search: 'great grandparent group' } }
+
+ it 'includes only the groups where the term matches the group name or path' do
+ expect(result).to contain_exactly(great_grandparent_group)
+ end
+ end
+
+ context 'when the feature flag `include_groups_from_group_shares_in_group_transfer_locations` is turned off' do
+ before do
+ stub_feature_flags(include_groups_from_group_shares_in_group_transfer_locations: false)
+ end
+
+ it 'excludes the groups where the user has OWNER access through group shares' do
+ expect(result).not_to include(
+ shared_with_group_where_direct_owner_as_owner,
+ subgroup_of_shared_with_group_where_direct_owner_as_owner
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb b/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb
index 8cdfa13ba3a..985b132ee8b 100644
--- a/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb
+++ b/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb
@@ -54,14 +54,14 @@ RSpec.describe Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershi
it 'includes only the expected projects' do
expected_projects = Project.id_in(
[
- project_b_subgroup_1, # direct member of Group B gets access to this project due to group hierarchy
- project_b_subgroup_2, # direct member of Group B gets access to this project due to group hierarchy
- project_c, # direct member of Group B gets access to this project via project-group share
- project_a_subgroup_1, # direct member of Group B gets access to this project via group share
- project_a_subgroup_2, # direct member of Group B gets access to this project via group share
+ project_b_subgroup_1, # direct member of Group B gets access to this project due to group hierarchy
+ project_b_subgroup_2, # direct member of Group B gets access to this project due to group hierarchy
+ project_c, # direct member of Group B gets access to this project via project-group share
+ project_a_subgroup_1, # direct member of Group B gets access to this project via group share
+ project_a_subgroup_2, # direct member of Group B gets access to this project via group share
- # direct member of Group B gets access to any projects shared with groups within its shared groups.
- project_x_subgroup_1
+ # direct member of Group B gets access to any projects shared with groups within its shared groups.
+ project_x_subgroup_1
]
)
# project_c_subgroup_1 is not included in the list because only 'direct' members of
diff --git a/spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb b/spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb
index 103cef44c94..1a0e8c5b9e6 100644
--- a/spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb
+++ b/spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb
@@ -46,9 +46,9 @@ RSpec.describe Groups::ProjectsRequiringAuthorizationsRefresh::OnTransferFinder
it 'includes only the expected projects' do
expected_projects = Project.id_in(
[
- project_b_subgroup_1,
- project_b_subgroup_2,
- project_c
+ project_b_subgroup_1,
+ project_b_subgroup_2,
+ project_c
]
)
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index a4cbee6a124..123df418f8d 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -261,6 +261,108 @@ RSpec.describe GroupsFinder do
end
end
end
+
+ context 'with include_ancestors' do
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:parent_group) { create(:group, :public) }
+ let_it_be(:public_subgroup) { create(:group, :public, parent: parent_group) }
+ let_it_be(:public_subgroup2) { create(:group, :public, parent: parent_group) }
+ let_it_be(:private_subgroup1) { create(:group, :private, parent: parent_group) }
+ let_it_be(:internal_sub_subgroup) { create(:group, :internal, parent: public_subgroup) }
+ let_it_be(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) }
+ let_it_be(:private_subgroup2) { create(:group, :private, parent: parent_group) }
+ let_it_be(:private_sub_subgroup) { create(:group, :private, parent: private_subgroup2) }
+ let_it_be(:private_sub_sub_subgroup) { create(:group, :private, parent: private_sub_subgroup) }
+
+ context 'if include_ancestors is true' do
+ let(:params) { { include_ancestors: true } }
+
+ it 'returns ancestors of user groups' do
+ private_sub_subgroup.add_developer(user)
+
+ expect(described_class.new(user, params).execute).to contain_exactly(
+ parent_group,
+ public_subgroup,
+ public_subgroup2,
+ internal_sub_subgroup,
+ public_sub_subgroup,
+ private_subgroup2,
+ private_sub_subgroup,
+ private_sub_sub_subgroup
+ )
+ end
+
+ it 'returns subgroup if user is member of project of subgroup' do
+ project = create(:project, :private, namespace: private_sub_subgroup)
+ project.add_developer(user)
+
+ expect(described_class.new(user, params).execute).to contain_exactly(
+ parent_group,
+ public_subgroup,
+ public_subgroup2,
+ internal_sub_subgroup,
+ public_sub_subgroup,
+ private_subgroup2,
+ private_sub_subgroup
+ )
+ end
+
+ it 'returns only groups related to user groups if all_available is false' do
+ params[:all_available] = false
+ private_sub_subgroup.add_developer(user)
+
+ expect(described_class.new(user, params).execute).to contain_exactly(
+ parent_group,
+ private_subgroup2,
+ private_sub_subgroup,
+ private_sub_sub_subgroup
+ )
+ end
+ end
+
+ context 'if include_ancestors is false' do
+ let(:params) { { include_ancestors: false } }
+
+ it 'does not return private ancestors of user groups' do
+ private_sub_subgroup.add_developer(user)
+
+ expect(described_class.new(user, params).execute).to contain_exactly(
+ parent_group,
+ public_subgroup,
+ public_subgroup2,
+ internal_sub_subgroup,
+ public_sub_subgroup,
+ private_sub_subgroup,
+ private_sub_sub_subgroup
+ )
+ end
+
+ it "returns project's parent group if user is member of project" do
+ project = create(:project, :private, namespace: private_sub_subgroup)
+ project.add_developer(user)
+
+ expect(described_class.new(user, params).execute).to contain_exactly(
+ parent_group,
+ public_subgroup,
+ public_subgroup2,
+ internal_sub_subgroup,
+ public_sub_subgroup,
+ private_sub_subgroup
+ )
+ end
+
+ it 'returns only user groups and their descendants if all_available is false' do
+ params[:all_available] = false
+ private_sub_subgroup.add_developer(user)
+
+ expect(described_class.new(user, params).execute).to contain_exactly(
+ private_sub_subgroup,
+ private_sub_sub_subgroup
+ )
+ end
+ end
+ end
end
describe '#execute' do
diff --git a/spec/finders/merge_requests_finder/params_spec.rb b/spec/finders/merge_requests_finder/params_spec.rb
deleted file mode 100644
index 8c285972f48..00000000000
--- a/spec/finders/merge_requests_finder/params_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequestsFinder::Params do
- let(:user) { create(:user) }
-
- subject { described_class.new(params, user, MergeRequest) }
-
- describe 'attention' do
- context 'attention param exists' do
- let(:params) { { attention: user.username } }
-
- it { expect(subject.attention).to eq(user) }
- end
-
- context 'attention param does not exist' do
- let(:params) { { attention: nil } }
-
- it { expect(subject.attention).to eq(nil) }
- end
- end
-end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 96466e99105..deeca6132e0 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -408,25 +408,6 @@ RSpec.describe MergeRequestsFinder do
end
end
- context 'attention' do
- subject { described_class.new(user, params).execute }
-
- before do
- reviewer = merge_request1.find_reviewer(user2)
- reviewer.update!(state: :reviewed)
-
- merge_request2.find_reviewer(user2).update!(state: :attention_requested)
- merge_request3.find_assignee(user2).update!(state: :attention_requested)
- end
-
- context 'by username' do
- let(:params) { { attention: user2.username } }
- let(:expected_mr) { [merge_request2, merge_request3] }
-
- it { is_expected.to contain_exactly(*expected_mr) }
- end
- end
-
context 'reviewer filtering' do
subject { described_class.new(user, params).execute }
@@ -512,7 +493,7 @@ RSpec.describe MergeRequestsFinder do
end
end
- context 'filtering by approved by' do
+ context 'filtering by approved by username' do
let(:params) { { approved_by_usernames: user2.username } }
before do
@@ -525,6 +506,16 @@ RSpec.describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request3)
end
+ context 'with sorting by milestone' do
+ let(:params) { { approved_by_usernames: user2.username, sort: 'milestone' } }
+
+ it 'returns merge requests approved by that user' do
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+ end
+
context 'not filter' do
let(:params) { { not: { approved_by_usernames: user2.username } } }
@@ -550,6 +541,30 @@ RSpec.describe MergeRequestsFinder do
end
end
+ context 'filtering by approved by user ID' do
+ let(:params) { { approved_by_ids: user2.id } }
+
+ before do
+ create(:approval, merge_request: merge_request3, user: user2)
+ end
+
+ it 'returns merge requests approved by that user' do
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+
+ context 'with sorting by milestone' do
+ let(:params) { { approved_by_usernames: user2.username, sort: 'milestone' } }
+
+ it 'returns merge requests approved by that user' do
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+ end
+ end
+
context 'filtering by created_at/updated_at' do
let(:new_project) { create(:project, forked_from_project: project1) }
@@ -751,7 +766,8 @@ RSpec.describe MergeRequestsFinder do
release_tag: 'none',
label_names: 'none',
my_reaction_emoji: 'none',
- draft: 'no'
+ draft: 'no',
+ sort: 'milestone'
}
merge_requests = described_class.new(user, params).execute
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index 8b26599cbfa..8dd83df3a28 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe MilestonesFinder do
end
context 'milestones for groups and project' do
- let(:extra_params) {{}}
+ let(:extra_params) { {} }
let(:result) do
described_class.new({ project_ids: [project_1.id, project_2.id], group_ids: group.id, state: 'all' }.merge(extra_params)).execute
end
diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb
index 90a8cd3c57f..f78a356b13d 100644
--- a/spec/finders/packages/group_packages_finder_spec.rb
+++ b/spec/finders/packages/group_packages_finder_spec.rb
@@ -217,7 +217,7 @@ RSpec.describe Packages::GroupPackagesFinder do
context 'group is nil' do
subject { described_class.new(user, nil).execute }
- it { is_expected.to be_empty}
+ it { is_expected.to be_empty }
end
context 'package type is nil' do
@@ -225,7 +225,7 @@ RSpec.describe Packages::GroupPackagesFinder do
subject { described_class.new(user, group, package_type: nil).execute }
- it { is_expected.to match_array([package1])}
+ it { is_expected.to match_array([package1]) }
end
context 'with invalid package_type' do
diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb
index 7fabb3eed86..8c9149a5a2d 100644
--- a/spec/finders/packages/npm/package_finder_spec.rb
+++ b/spec/finders/packages/npm/package_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ::Packages::Npm::PackageFinder do
- let_it_be_with_reload(:project) { create(:project)}
+ let_it_be_with_reload(:project) { create(:project) }
let_it_be_with_refind(:package) { create(:npm_package, project: project) }
let(:project) { package.project }
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 3bef4d85b33..1fa2a975ec3 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe ProjectsFinder do
describe 'with id_after' do
context 'only returns projects with a project id greater than given' do
- let(:params) { { id_after: internal_project.id }}
+ let(:params) { { id_after: internal_project.id } }
it { is_expected.to eq([public_project]) }
end
@@ -70,7 +70,7 @@ RSpec.describe ProjectsFinder do
describe 'with id_before' do
context 'only returns projects with a project id less than given' do
- let(:params) { { id_before: public_project.id }}
+ let(:params) { { id_before: public_project.id } }
it { is_expected.to eq([internal_project]) }
end
@@ -79,7 +79,7 @@ RSpec.describe ProjectsFinder do
describe 'with both id_before and id_after' do
context 'only returns projects with a project id less than given' do
let!(:projects) { create_list(:project, 5, :public) }
- let(:params) { { id_after: projects.first.id, id_before: projects.last.id }}
+ let(:params) { { id_after: projects.first.id, id_before: projects.last.id } }
it { is_expected.to contain_exactly(*projects[1..-2]) }
end
@@ -89,7 +89,7 @@ RSpec.describe ProjectsFinder do
context 'only returns projects with a project id less than given and matching search' do
subject { finder.execute.joins(:route) }
- let(:params) { { id_before: public_project.id }}
+ let(:params) { { id_before: public_project.id } }
it { is_expected.to eq([internal_project]) }
end
@@ -97,7 +97,7 @@ RSpec.describe ProjectsFinder do
context 'only returns projects with a project id greater than given and matching search' do
subject { finder.execute.joins(:route) }
- let(:params) { { id_after: internal_project.id }}
+ let(:params) { { id_after: internal_project.id } }
it { is_expected.to eq([public_project]) }
end
diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb
index 8e2426e697b..21fea7863ff 100644
--- a/spec/finders/template_finder_spec.rb
+++ b/spec/finders/template_finder_spec.rb
@@ -7,10 +7,10 @@ RSpec.describe TemplateFinder do
let_it_be(:template_files) do
{
- "Dockerfile/project_dockerfiles_template.dockerfile" => "project_dockerfiles_template content",
- "gitignore/project_gitignores_template.gitignore" => "project_gitignores_template content",
- "gitlab-ci/project_gitlab_ci_ymls_template.yml" => "project_gitlab_ci_ymls_template content",
- ".gitlab/issue_templates/project_issues_template.md" => "project_issues_template content",
+ "Dockerfile/project_dockerfiles_template.dockerfile" => "project_dockerfiles_template content",
+ "gitignore/project_gitignores_template.gitignore" => "project_gitignores_template content",
+ "gitlab-ci/project_gitlab_ci_ymls_template.yml" => "project_gitlab_ci_ymls_template content",
+ ".gitlab/issue_templates/project_issues_template.md" => "project_issues_template content",
".gitlab/merge_request_templates/project_merge_requests_template.md" => "project_merge_requests_template content"
}
end
diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
index 4ef19ed32c2..4b790a2c34b 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
@@ -12,6 +12,8 @@
"state": { "type": "string" },
"source_branch": { "type": "string" },
"target_branch": { "type": "string" },
+ "source_branch_path": { "type": "string" },
+ "target_branch_path": { "type": "string" },
"diff_head_sha": { "type": "string" },
"create_note_path": { "type": ["string", "null"] },
"preview_note_path": { "type": ["string", "null"] },
@@ -22,11 +24,13 @@
"type": "object",
"required": [
"can_create_note",
- "can_update"
+ "can_update",
+ "can_approve"
],
"properties": {
"can_create_note": { "type": "boolean" },
- "can_update": { "type": "boolean" }
+ "can_update": { "type": "boolean" },
+ "can_approve": { "type": "boolean" }
},
"additionalProperties": false
},
diff --git a/spec/fixtures/api/schemas/external_validation.json b/spec/fixtures/api/schemas/external_validation.json
index 4a2538a020e..411c2ed591b 100644
--- a/spec/fixtures/api/schemas/external_validation.json
+++ b/spec/fixtures/api/schemas/external_validation.json
@@ -3,6 +3,7 @@
"required" : [
"project",
"user",
+ "credit_card",
"pipeline",
"builds",
"total_builds_count"
@@ -43,6 +44,17 @@
"sign_in_count": { "type": "integer" }
}
},
+ "credit_card": {
+ "type": "object",
+ "required": [
+ "similar_cards_count",
+ "similar_holder_names_count"
+ ],
+ "properties": {
+ "similar_cards_count": { "type": "integer" },
+ "similar_holder_names_count": { "type": "integer" }
+ }
+ },
"pipeline": {
"type": "object",
"required": [
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 50e52a7bb87..33eb67a0280 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -13,7 +13,8 @@
"pipelines",
"versions",
"status",
- "canDestroy"
+ "canDestroy",
+ "lastDownloadedAt"
],
"properties": {
"id": {
@@ -173,6 +174,9 @@
},
"composerConfigRepositoryUrl": {
"type": "string"
+ },
+ "lastDownloadedAt": {
+ "type": ["string", "null"]
}
}
}
diff --git a/spec/fixtures/api/schemas/ml/get_experiment.json b/spec/fixtures/api/schemas/ml/get_experiment.json
new file mode 100644
index 00000000000..cf8da7f999f
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/get_experiment.json
@@ -0,0 +1,23 @@
+{
+ "type": "object",
+ "required": [
+ "experiment"
+ ],
+ "properties": {
+ "experiment": {
+ "type": "object",
+ "required" : [
+ "experiment_id",
+ "name",
+ "artifact_location",
+ "lifecycle_stage"
+ ],
+ "properties" : {
+ "experiment_id": { "type": "string" },
+ "name": { "type": "string" },
+ "artifact_location": { "type": "string" },
+ "lifecycle_stage": { "type": { "enum" : ["active", "deleted"] } }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/run.json b/spec/fixtures/api/schemas/ml/run.json
new file mode 100644
index 00000000000..2418f44b21f
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/run.json
@@ -0,0 +1,47 @@
+{
+ "type": "object",
+ "required": [
+ "run"
+ ],
+ "properties": {
+ "run": {
+ "type": "object",
+ "required": [
+ "info",
+ "data"
+ ],
+ "properties": {
+ "info": {
+ "type": "object",
+ "required": [
+ "run_id",
+ "run_uuid",
+ "user_id",
+ "experiment_id",
+ "status",
+ "start_time",
+ "artifact_uri",
+ "lifecycle_stage"
+ ],
+ "optional": [
+ "end_time"
+ ],
+ "properties": {
+ "run_id": { "type": "string" },
+ "run_uuid": { "type": "string" },
+ "experiment_id": { "type": "string" },
+ "artifact_location": { "type": "string" },
+ "start_time": { "type": "integer" },
+ "end_time": { "type": "integer" },
+ "user_id": "",
+ "status": { "type": { "enum" : ["RUNNING", "SCHEDULED", "FINISHED", "FAILED", "KILLED"] } },
+ "lifecycle_stage": { "type": { "enum" : ["active"] } }
+ }
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/update_run.json b/spec/fixtures/api/schemas/ml/update_run.json
new file mode 100644
index 00000000000..b429444120f
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/update_run.json
@@ -0,0 +1,35 @@
+{
+ "type": "object",
+ "required": [
+ "run_info"
+ ],
+ "properties": {
+ "run_info": {
+ "type": "object",
+ "required": [
+ "run_id",
+ "run_uuid",
+ "user_id",
+ "experiment_id",
+ "status",
+ "start_time",
+ "artifact_uri",
+ "lifecycle_stage"
+ ],
+ "optional": [
+ "end_time"
+ ],
+ "properties": {
+ "run_id": { "type": "string" },
+ "run_uuid": { "type": "string" },
+ "experiment_id": { "type": "string" },
+ "artifact_location": { "type": "string" },
+ "start_time": { "type": "integer" },
+ "end_time": { "type": "integer" },
+ "user_id": { "type": "string" },
+ "status": { "type": { "enum" : ["RUNNING", "SCHEDULED", "FINISHED", "FAILED", "KILLED"] } },
+ "lifecycle_stage": { "type": { "enum" : ["active"] } }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json
index afed4f23017..f6b12d3a1c0 100644
--- a/spec/fixtures/api/schemas/public_api/v4/job.json
+++ b/spec/fixtures/api/schemas/public_api/v4/job.json
@@ -20,7 +20,8 @@
"artifacts",
"artifacts_expire_at",
"tag_list",
- "runner"
+ "runner",
+ "project"
],
"properties": {
"id": { "type": "integer" },
@@ -64,6 +65,9 @@
{ "type": "null" },
{ "$ref": "runner.json" }
]
+ },
+ "project": {
+ "ci_job_token_scope_enabled": { "type": "boolean" }
}
},
"additionalProperties":false
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/package.json b/spec/fixtures/api/schemas/public_api/v4/packages/package.json
index 607e0df1886..5d0d5f63aa9 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/package.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/package.json
@@ -6,7 +6,9 @@
"package_type",
"status",
"_links",
- "versions"
+ "versions",
+ "created_at",
+ "last_downloaded_at"
],
"properties": {
"name": {
@@ -38,6 +40,9 @@
"created_at": {
"type": "string"
},
+ "last_downloaded_at": {
+ "type": ["string", "null"]
+ },
"versions": {
"type": "array",
"items": {
diff --git a/spec/fixtures/blockquote_fence_after.md b/spec/fixtures/blockquote_fence_after.md
index 555905bf07e..18500d94c7a 100644
--- a/spec/fixtures/blockquote_fence_after.md
+++ b/spec/fixtures/blockquote_fence_after.md
@@ -129,3 +129,27 @@ Double `>>>` inside HTML inside blockquote:
>
> Quote
+
+Requires a leading blank line
+>>>
+Not a quote
+>>>
+
+Requires a trailing blank line
+
+>>>
+Not a quote
+>>>
+Lorem
+
+Triple quoting is not our blockquote
+
+>>> foo
+>>> bar
+>>>
+> baz
+
+> boo
+>>> far
+>>>
+>>> faz
diff --git a/spec/fixtures/blockquote_fence_before.md b/spec/fixtures/blockquote_fence_before.md
index d52eec72896..895bff73404 100644
--- a/spec/fixtures/blockquote_fence_before.md
+++ b/spec/fixtures/blockquote_fence_before.md
@@ -129,3 +129,27 @@ Quote
Quote
>>>
+
+Requires a leading blank line
+>>>
+Not a quote
+>>>
+
+Requires a trailing blank line
+
+>>>
+Not a quote
+>>>
+Lorem
+
+Triple quoting is not our blockquote
+
+>>> foo
+>>> bar
+>>>
+> baz
+
+> boo
+>>> far
+>>>
+>>> faz
diff --git a/spec/fixtures/cdn/google_cloud.json b/spec/fixtures/cdn/google_cloud.json
new file mode 100644
index 00000000000..8c3f25d805f
--- /dev/null
+++ b/spec/fixtures/cdn/google_cloud.json
@@ -0,0 +1,17 @@
+{
+ "syncToken": "1661533328840",
+ "creationTime": "2022-08-26T10:02:08.840384",
+ "prefixes": [{
+ "ipv4Prefix": "34.80.0.0/15",
+ "service": "Google Cloud",
+ "scope": "asia-east1"
+ }, {
+ "ipv4Prefix": "34.137.0.0/16",
+ "service": "Google Cloud",
+ "scope": "asia-east1"
+ }, {
+ "ipv6Prefix": "2600:1900:4180::/44",
+ "service": "Google Cloud",
+ "scope": "us-west4"
+ }]
+}
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
index bc7df779a58..e15336f586e 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
@@ -3,5 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountFooMetric do
- it_behaves_like 'a correct instrumented metric value', {}, 1
+ let(:expected_value) { 1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
end
diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zip
deleted file mode 100644
index 31124abc0e5..00000000000
--- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zip
deleted file mode 100644
index 8c56cce641a..00000000000
--- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip
deleted file mode 100644
index 09ac4e5df51..00000000000
--- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip
deleted file mode 100644
index 81768a9f2b3..00000000000
--- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zip
deleted file mode 100644
index 6de321ea86a..00000000000
--- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zip
deleted file mode 100644
index b8cfcef9739..00000000000
--- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index 5bcf6521471..f3fc69e4936 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -394,7 +394,41 @@
"id": 1,
"issue_id": 40,
"sentry_issue_identifier": 1234567891
- }
+ },
+ "resource_milestone_events": [
+ {
+ "user_id": 1,
+ "action": "add",
+ "state": "opened",
+ "created_at": "2022-08-17T13:06:53.547Z",
+ "milestone": {
+ "title": "v4.0",
+ "description": "Totam quam laborum id magnam natus eaque aspernatur.",
+ "created_at": "2016-06-14T15:02:04.590Z",
+ "updated_at": "2016-06-14T15:02:04.590Z",
+ "state": "active",
+ "iid": 5
+ }
+ }
+ ],
+ "resource_state_events": [
+ {
+ "user_id": 1,
+ "created_at": "2022-08-17T13:08:16.838Z",
+ "state": "closed",
+ "source_commit": null,
+ "close_after_error_tracking_resolve": false,
+ "close_auto_resolve_prometheus_alert": false
+ },
+ {
+ "user_id": 1,
+ "created_at": "2022-08-17T13:08:17.702Z",
+ "state": "reopened",
+ "source_commit": null,
+ "close_after_error_tracking_resolve": false,
+ "close_auto_resolve_prometheus_alert": false
+ }
+ ]
},
{
"id": 39,
@@ -3164,7 +3198,7 @@
{
"user_id": 16,
"created_at": "2020-01-09T11:21:21.235Z",
- "state": "attention_requested"
+ "state": "reviewed"
},
{
"user_id": 6,
@@ -3186,7 +3220,7 @@
{
"user_id": 16,
"created_at": "2020-01-09T11:21:21.235Z",
- "state": "attention_requested"
+ "state": "reviewed"
},
{
"user_id": 6,
@@ -3215,6 +3249,40 @@
"created_at": "2020-01-07T11:21:21.235Z",
"updated_at": "2020-01-08T11:21:21.235Z"
}
+ ],
+ "resource_milestone_events": [
+ {
+ "user_id": 1,
+ "action": "add",
+ "state": "opened",
+ "created_at": "2022-08-17T13:06:53.547Z",
+ "milestone": {
+ "title": "v4.0",
+ "description": "Totam quam laborum id magnam natus eaque aspernatur.",
+ "created_at": "2016-06-14T15:02:04.590Z",
+ "updated_at": "2016-06-14T15:02:04.590Z",
+ "state": "active",
+ "iid": 5
+ }
+ }
+ ],
+ "resource_state_events": [
+ {
+ "user_id": 1,
+ "created_at": "2022-08-17T13:08:16.838Z",
+ "state": "closed",
+ "source_commit": null,
+ "close_after_error_tracking_resolve": false,
+ "close_auto_resolve_prometheus_alert": false
+ },
+ {
+ "user_id": 1,
+ "created_at": "2022-08-17T13:08:17.702Z",
+ "state": "reopened",
+ "source_commit": null,
+ "close_after_error_tracking_resolve": false,
+ "close_auto_resolve_prometheus_alert": false
+ }
]
},
{
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
index 2ebd1a78783..3955107865d 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
@@ -1,4 +1,4 @@
-{"id":40,"title":"Voluptatem","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.340Z","updated_at":"2016-06-14T15:02:47.967Z","position":0,"branch_name":null,"description":"Aliquam enim illo et possimus.","state":"opened","iid":10,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"test_ee_field":"test","issue_assignees":[{"user_id":1,"issue_id":40},{"user_id":15,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":6,"issue_id":40}],"award_emoji":[{"id":1,"name":"musical_keyboard","user_id":1,"awardable_type":"Issue","awardable_id":40,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}],"zoom_meetings":[{"id":1,"project_id":5,"issue_id":40,"url":"https://zoom.us/j/123456789","issue_status":1,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z"}],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"label_links":[{"id":2,"label_id":2,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}},{"id":3,"label_id":3,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.841Z","updated_at":"2016-07-22T08:57:02.841Z","label":{"id":3,"title":"test3","color":"#428bca","group_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","project_id":null,"type":"GroupLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}}],"notes":[{"id":351,"note":"Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:47.770Z","updated_at":"2016-06-14T15:02:47.770Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"clapper","user_id":1,"awardable_type":"Note","awardable_id":351,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}]},{"id":352,"note":"Est reprehenderit quas aut aspernatur autem recusandae voluptatem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:47.795Z","updated_at":"2016-06-14T15:02:47.795Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":353,"note":"Perspiciatis suscipit voluptates in eius nihil.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:47.823Z","updated_at":"2016-06-14T15:02:47.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":354,"note":"Aut vel voluptas corrupti nisi provident laboriosam magnam aut.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:47.850Z","updated_at":"2016-06-14T15:02:47.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":355,"note":"Officia dolore consequatur in saepe cum magni.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:47.876Z","updated_at":"2016-06-14T15:02:47.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":356,"note":"Cum ipsum rem voluptas eaque et ea.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:47.908Z","updated_at":"2016-06-14T15:02:47.908Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":357,"note":"Recusandae excepturi asperiores suscipit autem nostrum.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:47.937Z","updated_at":"2016-06-14T15:02:47.937Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":358,"note":"Et hic est id similique et non nesciunt voluptate.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:47.965Z","updated_at":"2016-06-14T15:02:47.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":244,"action":"remove","issue_id":40,"merge_request_id":null,"label_id":2,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"sentry_issue":{"id":1,"issue_id":40,"sentry_issue_identifier":1234567891}}
+{"id":40,"title":"Voluptatem","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.340Z","updated_at":"2016-06-14T15:02:47.967Z","position":0,"branch_name":null,"description":"Aliquam enim illo et possimus.","state":"opened","iid":10,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"test_ee_field":"test","issue_assignees":[{"user_id":1,"issue_id":40},{"user_id":15,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":6,"issue_id":40}],"award_emoji":[{"id":1,"name":"musical_keyboard","user_id":1,"awardable_type":"Issue","awardable_id":40,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}],"zoom_meetings":[{"id":1,"project_id":5,"issue_id":40,"url":"https://zoom.us/j/123456789","issue_status":1,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z"}],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"label_links":[{"id":2,"label_id":2,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}},{"id":3,"label_id":3,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.841Z","updated_at":"2016-07-22T08:57:02.841Z","label":{"id":3,"title":"test3","color":"#428bca","group_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","project_id":null,"type":"GroupLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}}],"notes":[{"id":351,"note":"Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:47.770Z","updated_at":"2016-06-14T15:02:47.770Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"clapper","user_id":1,"awardable_type":"Note","awardable_id":351,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}]},{"id":352,"note":"Est reprehenderit quas aut aspernatur autem recusandae voluptatem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:47.795Z","updated_at":"2016-06-14T15:02:47.795Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":353,"note":"Perspiciatis suscipit voluptates in eius nihil.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:47.823Z","updated_at":"2016-06-14T15:02:47.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":354,"note":"Aut vel voluptas corrupti nisi provident laboriosam magnam aut.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:47.850Z","updated_at":"2016-06-14T15:02:47.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":355,"note":"Officia dolore consequatur in saepe cum magni.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:47.876Z","updated_at":"2016-06-14T15:02:47.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":356,"note":"Cum ipsum rem voluptas eaque et ea.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:47.908Z","updated_at":"2016-06-14T15:02:47.908Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":357,"note":"Recusandae excepturi asperiores suscipit autem nostrum.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:47.937Z","updated_at":"2016-06-14T15:02:47.937Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":358,"note":"Et hic est id similique et non nesciunt voluptate.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:47.965Z","updated_at":"2016-06-14T15:02:47.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":244,"action":"remove","issue_id":40,"merge_request_id":null,"label_id":2,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"sentry_issue":{"id":1,"issue_id":40,"sentry_issue_identifier":1234567891},"resource_milestone_events":[{"user_id":1,"action":"add","state":"opened","created_at":"2022-08-17T13:06:53.547Z","milestone":{"title":"v4.0","description":"Totam quam laborum id magnam natus eaque aspernatur.","created_at":"2016-06-14T15:02:04.590Z","updated_at":"2016-06-14T15:02:04.590Z","state":"active","iid":5}}],"resource_state_events":[{"user_id":1,"created_at":"2022-08-17T13:08:16.838Z","state":"closed","source_commit":null,"close_after_error_tracking_resolve":false,"close_auto_resolve_prometheus_alert":false},{"user_id":1,"created_at":"2022-08-17T13:08:17.702Z","state":"reopened","source_commit":null,"close_after_error_tracking_resolve":false,"close_auto_resolve_prometheus_alert":false}]}
{"id":39,"title":"Issue without assignees","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.233Z","updated_at":"2016-06-14T15:02:48.194Z","position":0,"branch_name":null,"description":"Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.","state":"opened","iid":9,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"notes":[{"id":359,"note":"Quo eius velit quia et id quam.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.009Z","updated_at":"2016-06-14T15:02:48.009Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":360,"note":"Nulla commodi ratione cumque id autem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.032Z","updated_at":"2016-06-14T15:02:48.032Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":361,"note":"Illum non ea sed dolores corrupti.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.056Z","updated_at":"2016-06-14T15:02:48.056Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":362,"note":"Facere dolores ipsum dolorum maiores omnis occaecati ab.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.082Z","updated_at":"2016-06-14T15:02:48.082Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":363,"note":"Quod laudantium similique sint aut est ducimus.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.113Z","updated_at":"2016-06-14T15:02:48.113Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":364,"note":"Aut omnis eos esse incidunt vero reiciendis.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.139Z","updated_at":"2016-06-14T15:02:48.139Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":365,"note":"Beatae dolore et doloremque asperiores sunt.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.162Z","updated_at":"2016-06-14T15:02:48.162Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":366,"note":"Doloribus ipsam ex delectus rerum libero recusandae modi repellendus.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.192Z","updated_at":"2016-06-14T15:02:48.192Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
{"id":38,"title":"Quasi adipisci non cupiditate dolorem quo qui earum sed.","author_id":6,"project_id":5,"created_at":"2016-06-14T15:02:08.154Z","updated_at":"2016-06-14T15:02:48.614Z","position":0,"branch_name":null,"description":"Ea recusandae neque autem tempora.","state":"closed","iid":8,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"label_links":[{"id":99,"label_id":2,"target_id":38,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"notes":[{"id":367,"note":"Accusantium fugiat et eaque quisquam esse corporis.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.235Z","updated_at":"2016-06-14T15:02:48.235Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":368,"note":"Ea labore eum nam qui laboriosam.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.261Z","updated_at":"2016-06-14T15:02:48.261Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":369,"note":"Accusantium quis sed molestiae et.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.294Z","updated_at":"2016-06-14T15:02:48.294Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":370,"note":"Corporis numquam a voluptatem pariatur asperiores dolorem delectus autem.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.523Z","updated_at":"2016-06-14T15:02:48.523Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":371,"note":"Ea accusantium maxime voluptas rerum.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.546Z","updated_at":"2016-06-14T15:02:48.546Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":372,"note":"Pariatur iusto et et excepturi similique ipsam eum.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.569Z","updated_at":"2016-06-14T15:02:48.569Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":373,"note":"Aliquam et culpa officia iste eius.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.591Z","updated_at":"2016-06-14T15:02:48.591Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":374,"note":"Ab id velit id unde laborum.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.613Z","updated_at":"2016-06-14T15:02:48.613Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
{"id":37,"title":"Cupiditate quo aut ducimus minima molestiae vero numquam possimus.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:08.051Z","updated_at":"2016-06-14T15:02:48.854Z","position":0,"branch_name":null,"description":"Maiores architecto quos in dolorem.","state":"opened","iid":7,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":375,"note":"Quasi fugit qui sed eligendi aut quia.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.647Z","updated_at":"2016-06-14T15:02:48.647Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":376,"note":"Esse nesciunt voluptatem ex vero est consequatur.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.674Z","updated_at":"2016-06-14T15:02:48.674Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":377,"note":"Similique qui quas non aut et velit sequi in.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.696Z","updated_at":"2016-06-14T15:02:48.696Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":378,"note":"Eveniet ut cupiditate repellendus numquam in esse eius.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.720Z","updated_at":"2016-06-14T15:02:48.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":379,"note":"Velit est dolorem adipisci rerum sed iure.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.755Z","updated_at":"2016-06-14T15:02:48.755Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":380,"note":"Voluptatem ullam ab ut illo ut quo.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.793Z","updated_at":"2016-06-14T15:02:48.793Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":381,"note":"Voluptatem impedit beatae quasi ipsa earum consectetur.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.823Z","updated_at":"2016-06-14T15:02:48.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":382,"note":"Nihil officiis eaque incidunt sunt voluptatum excepturi.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.852Z","updated_at":"2016-06-14T15:02:48.852Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
index c14221adc1c..887d7ab658b 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
@@ -1,4 +1,4 @@
-{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":true,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":669,"note":"added 3 commits\n\n<ul><li>16ea4e20...074a2a32 - 2 commits from branch <code>master</code></li><li>ca223a02 - readme: fix typos</li></ul>\n\n[Compare with previous version](/group/project/merge_requests/1/diffs?diff_id=1189&start_sha=16ea4e207fb258fe4e9c73185a725207c9a4f3e1)","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4789,"commit_count":3,"action":"commit","created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z"},"events":[],"suggestions":[]},{"id":670,"note":"unmarked as a **Work In Progress**","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4790,"commit_count":null,"action":"title","created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z"},"events":[],"suggestions":[]},{"id":671,"note":"Sit voluptatibus eveniet architecto quidem.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.632Z","updated_at":"2016-06-14T15:02:56.632Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"tada","user_id":1,"awardable_type":"Note","awardable_id":1,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]},{"id":672,"note":"Odio maxime ratione voluptatibus sed.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.656Z","updated_at":"2016-06-14T15:02:56.656Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":673,"note":"Et deserunt et omnis nihil excepturi accusantium.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.679Z","updated_at":"2016-06-14T15:02:56.679Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":674,"note":"Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.700Z","updated_at":"2016-06-14T15:02:56.700Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[],"suggestions":[{"id":1,"note_id":674,"relative_order":0,"applied":false,"commit_id":null,"from_content":"Original line\n","to_content":"New line\n","lines_above":0,"lines_below":0,"outdated":false}]},{"id":675,"note":"Numquam est at dolor quo et sed eligendi similique.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.720Z","updated_at":"2016-06-14T15:02:56.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":676,"note":"Et perferendis aliquam sunt nisi labore delectus.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.742Z","updated_at":"2016-06-14T15:02:56.742Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":677,"note":"Aut ex rerum et in.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.791Z","updated_at":"2016-06-14T15:02:56.791Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":678,"note":"Dolor laborum earum ut exercitationem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:56.814Z","updated_at":"2016-06-14T15:02:56.814Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":243,"action":"add","issue_id":null,"merge_request_id":27,"label_id":null,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z"}],"merge_request_diff":{"id":27,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":27,"relative_order":0,"sha":"bb5206fee213d983da88c47f9cf4cc6caf9c66dc","message":"Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-08-06T08:35:52.000+02:00","committed_date":"2014-08-06T08:35:52.000+02:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":1,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","committed_date":"2014-02-27T10:01:38.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":2,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","committed_date":"2014-02-27T09:57:31.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":3,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","committed_date":"2014-02-27T09:54:21.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":4,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","committed_date":"2014-02-27T09:49:50.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":5,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","committed_date":"2014-02-27T09:48:32.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":27,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":3,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":5,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":6,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":8,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":27,"created_at":"2016-06-14T15:02:36.572Z","updated_at":"2016-06-14T15:02:36.658Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"9"},"events":[{"id":221,"target_type":"MergeRequest","target_id":27,"project_id":36,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1},{"id":187,"target_type":"MergeRequest","target_id":27,"project_id":5,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1}],"approvals_before_merge":1,"award_emoji":[{"id":1,"name":"thumbsup","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"},{"id":2,"name":"drum","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"}],"merge_request_assignees":[{"user_id":1,"created_at":"2020-01-07T11:21:21.235Z","state":"unreviewed"},{"user_id":15,"created_at":"2020-01-08T11:21:21.235Z","state":"reviewed"},{"user_id":16,"created_at":"2020-01-09T11:21:21.235Z","state":"attention_requested"},{"user_id":6,"created_at":"2020-01-10T11:21:21.235Z","state":"unreviewed"}],"merge_request_reviewers":[{"user_id":1,"created_at":"2020-01-07T11:21:21.235Z","state":"unreviewed"},{"user_id":15,"created_at":"2020-01-08T11:21:21.235Z","state":"reviewed"},{"user_id":16,"created_at":"2020-01-09T11:21:21.235Z","state":"attention_requested"},{"user_id":6,"created_at":"2020-01-10T11:21:21.235Z","state":"unreviewed"}],"approvals":[{"user_id":1,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"},{"user_id":15,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"},{"user_id":16,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"},{"user_id":6,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"}]}
+{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":true,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":669,"note":"added 3 commits\n\n<ul><li>16ea4e20...074a2a32 - 2 commits from branch <code>master</code></li><li>ca223a02 - readme: fix typos</li></ul>\n\n[Compare with previous version](/group/project/merge_requests/1/diffs?diff_id=1189&start_sha=16ea4e207fb258fe4e9c73185a725207c9a4f3e1)","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4789,"commit_count":3,"action":"commit","created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z"},"events":[],"suggestions":[]},{"id":670,"note":"unmarked as a **Work In Progress**","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4790,"commit_count":null,"action":"title","created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z"},"events":[],"suggestions":[]},{"id":671,"note":"Sit voluptatibus eveniet architecto quidem.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.632Z","updated_at":"2016-06-14T15:02:56.632Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"tada","user_id":1,"awardable_type":"Note","awardable_id":1,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]},{"id":672,"note":"Odio maxime ratione voluptatibus sed.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.656Z","updated_at":"2016-06-14T15:02:56.656Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":673,"note":"Et deserunt et omnis nihil excepturi accusantium.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.679Z","updated_at":"2016-06-14T15:02:56.679Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":674,"note":"Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.700Z","updated_at":"2016-06-14T15:02:56.700Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[],"suggestions":[{"id":1,"note_id":674,"relative_order":0,"applied":false,"commit_id":null,"from_content":"Original line\n","to_content":"New line\n","lines_above":0,"lines_below":0,"outdated":false}]},{"id":675,"note":"Numquam est at dolor quo et sed eligendi similique.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.720Z","updated_at":"2016-06-14T15:02:56.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":676,"note":"Et perferendis aliquam sunt nisi labore delectus.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.742Z","updated_at":"2016-06-14T15:02:56.742Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":677,"note":"Aut ex rerum et in.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.791Z","updated_at":"2016-06-14T15:02:56.791Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":678,"note":"Dolor laborum earum ut exercitationem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:56.814Z","updated_at":"2016-06-14T15:02:56.814Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":243,"action":"add","issue_id":null,"merge_request_id":27,"label_id":null,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z"}],"merge_request_diff":{"id":27,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":27,"relative_order":0,"sha":"bb5206fee213d983da88c47f9cf4cc6caf9c66dc","message":"Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-08-06T08:35:52.000+02:00","committed_date":"2014-08-06T08:35:52.000+02:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":1,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","committed_date":"2014-02-27T10:01:38.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":2,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","committed_date":"2014-02-27T09:57:31.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":3,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","committed_date":"2014-02-27T09:54:21.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":4,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","committed_date":"2014-02-27T09:49:50.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":5,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","committed_date":"2014-02-27T09:48:32.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":27,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":3,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":5,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":6,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":8,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":27,"created_at":"2016-06-14T15:02:36.572Z","updated_at":"2016-06-14T15:02:36.658Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"9"},"events":[{"id":221,"target_type":"MergeRequest","target_id":27,"project_id":36,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1},{"id":187,"target_type":"MergeRequest","target_id":27,"project_id":5,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1}],"approvals_before_merge":1,"award_emoji":[{"id":1,"name":"thumbsup","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"},{"id":2,"name":"drum","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"}],"merge_request_assignees":[{"user_id":1,"created_at":"2020-01-07T11:21:21.235Z","state":"unreviewed"},{"user_id":15,"created_at":"2020-01-08T11:21:21.235Z","state":"reviewed"},{"user_id":16,"created_at":"2020-01-09T11:21:21.235Z","state":"reviewed"},{"user_id":6,"created_at":"2020-01-10T11:21:21.235Z","state":"unreviewed"}],"merge_request_reviewers":[{"user_id":1,"created_at":"2020-01-07T11:21:21.235Z","state":"unreviewed"},{"user_id":15,"created_at":"2020-01-08T11:21:21.235Z","state":"reviewed"},{"user_id":16,"created_at":"2020-01-09T11:21:21.235Z","state":"reviewed"},{"user_id":6,"created_at":"2020-01-10T11:21:21.235Z","state":"unreviewed"}],"approvals":[{"user_id":1,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"},{"user_id":15,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"},{"user_id":16,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"},{"user_id":6,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-08T11:21:21.235Z"}],"resource_milestone_events":[{"user_id":1,"action":"add","state":"opened","created_at":"2022-08-17T13:06:53.547Z","milestone":{"title":"v4.0","description":"Totam quam laborum id magnam natus eaque aspernatur.","created_at":"2016-06-14T15:02:04.590Z","updated_at":"2016-06-14T15:02:04.590Z","state":"active","iid":5}}],"resource_state_events":[{"user_id":1,"created_at":"2022-08-17T13:08:16.838Z","state":"closed","source_commit":null,"close_after_error_tracking_resolve":false,"close_auto_resolve_prometheus_alert":false},{"user_id":1,"created_at":"2022-08-17T13:08:17.702Z","state":"reopened","source_commit":null,"close_after_error_tracking_resolve":false,"close_auto_resolve_prometheus_alert":false}]}
{"id":26,"target_branch":"master","source_branch":"feature","source_project_id":4,"author_id":1,"assignee_id":null,"title":"MR2","created_at":"2016-06-14T15:02:36.418Z","updated_at":"2016-06-14T15:02:57.013Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":8,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":679,"note":"Qui rerum totam nisi est.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.848Z","updated_at":"2016-06-14T15:02:56.848Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":680,"note":"Pariatur magni corrupti consequatur debitis minima error beatae voluptatem.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.871Z","updated_at":"2016-06-14T15:02:56.871Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":681,"note":"Qui quis ut modi eos rerum ratione.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.895Z","updated_at":"2016-06-14T15:02:56.895Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":682,"note":"Illum quidem expedita mollitia fugit.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.918Z","updated_at":"2016-06-14T15:02:56.918Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":683,"note":"Consectetur voluptate sit sint possimus veritatis quod.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.942Z","updated_at":"2016-06-14T15:02:56.942Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":684,"note":"Natus libero quibusdam rem assumenda deleniti accusamus sed earum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.966Z","updated_at":"2016-06-14T15:02:56.966Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":685,"note":"Tenetur autem nihil rerum odit.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.989Z","updated_at":"2016-06-14T15:02:56.989Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":686,"note":"Quia maiores et odio sed.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:57.012Z","updated_at":"2016-06-14T15:02:57.012Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":26,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":26,"sha":"0b4bc9a49b562e85de7cc9e834518ea6828729b9","relative_order":0,"message":"Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:26:01.000+01:00","committed_date":"2014-02-27T09:26:01.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":26,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":26,"created_at":"2016-06-14T15:02:36.421Z","updated_at":"2016-06-14T15:02:36.474Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"1"},"events":[{"id":222,"target_type":"MergeRequest","target_id":26,"project_id":36,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1},{"id":186,"target_type":"MergeRequest","target_id":26,"project_id":5,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1}],"merge_request_assignees":[],"merge_request_reviewers":[],"approvals":[]}
{"id":15,"target_branch":"test-7","source_branch":"test-1","source_project_id":5,"author_id":22,"assignee_id":16,"title":"Qui accusantium et inventore facilis doloribus occaecati officiis.","created_at":"2016-06-14T15:02:25.168Z","updated_at":"2016-06-14T15:02:59.521Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":7,"description":"Et commodi deserunt aspernatur vero rerum. Ut non dolorum alias in odit est libero. Voluptatibus eos in et vitae repudiandae facilis ex mollitia.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":777,"note":"Pariatur voluptas placeat aspernatur culpa suscipit soluta.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.348Z","updated_at":"2016-06-14T15:02:59.348Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":778,"note":"Alias et iure mollitia suscipit molestiae voluptatum nostrum asperiores.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.372Z","updated_at":"2016-06-14T15:02:59.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":779,"note":"Laudantium qui eum qui sunt.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.395Z","updated_at":"2016-06-14T15:02:59.395Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":780,"note":"Quas rem est iusto ut delectus fugiat recusandae mollitia.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.418Z","updated_at":"2016-06-14T15:02:59.418Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":781,"note":"Repellendus ab et qui nesciunt.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.444Z","updated_at":"2016-06-14T15:02:59.444Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":782,"note":"Non possimus voluptatum odio qui ut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.469Z","updated_at":"2016-06-14T15:02:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":783,"note":"Dolores repellendus eum ducimus quam ab dolorem quia.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.494Z","updated_at":"2016-06-14T15:02:59.494Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":784,"note":"Facilis dolorem aut corrupti id ratione occaecati.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.520Z","updated_at":"2016-06-14T15:02:59.520Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":15,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":15,"relative_order":0,"sha":"94b8d581c48d894b86661718582fecbc5e3ed2eb","message":"fixes #10\n","authored_date":"2016-01-19T13:22:56.000+01:00","committed_date":"2016-01-19T13:22:56.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}}],"merge_request_diff_files":[{"merge_request_diff_id":15,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":15,"created_at":"2016-06-14T15:02:25.171Z","updated_at":"2016-06-14T15:02:25.230Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":223,"target_type":"MergeRequest","target_id":15,"project_id":36,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":1},{"id":175,"target_type":"MergeRequest","target_id":15,"project_id":5,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":22}]}
{"id":14,"target_branch":"fix","source_branch":"test-3","source_project_id":5,"author_id":20,"assignee_id":20,"title":"In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.","created_at":"2016-06-14T15:02:24.760Z","updated_at":"2016-06-14T15:02:59.749Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":6,"description":"Dicta magnam non voluptates nam dignissimos nostrum deserunt. Dolorum et suscipit iure quae doloremque. Necessitatibus saepe aut labore sed.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":785,"note":"Atque cupiditate necessitatibus deserunt minus natus odit.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.559Z","updated_at":"2016-06-14T15:02:59.559Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":786,"note":"Non dolorem provident mollitia nesciunt optio ex eveniet.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.587Z","updated_at":"2016-06-14T15:02:59.587Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":787,"note":"Similique officia nemo quasi commodi accusantium quae qui.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.621Z","updated_at":"2016-06-14T15:02:59.621Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":788,"note":"Et est et alias ad dolor qui.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.650Z","updated_at":"2016-06-14T15:02:59.650Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":789,"note":"Numquam temporibus ratione voluptatibus aliquid.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.675Z","updated_at":"2016-06-14T15:02:59.675Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":790,"note":"Ut ex aliquam consectetur perferendis est hic aut quia.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.703Z","updated_at":"2016-06-14T15:02:59.703Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":791,"note":"Esse eos quam quaerat aut ut asperiores officiis.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.726Z","updated_at":"2016-06-14T15:02:59.726Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":792,"note":"Sint facilis accusantium iure blanditiis.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.748Z","updated_at":"2016-06-14T15:02:59.748Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":14,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":14,"relative_order":0,"sha":"ddd4ff416a931589c695eb4f5b23f844426f6928","message":"fixes #10\n","authored_date":"2016-01-19T14:14:43.000+01:00","committed_date":"2016-01-19T14:14:43.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":14,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","committed_date":"2015-12-07T12:52:12.000+01:00","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":14,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","committed_date":"2015-12-07T11:54:28.000+01:00","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":14,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","committed_date":"2015-11-13T16:27:12.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","committed_date":"2015-11-13T08:50:17.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","committed_date":"2015-11-13T08:39:43.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","committed_date":"2015-11-13T07:21:40.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","committed_date":"2015-11-13T06:01:27.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","committed_date":"2015-11-13T06:00:16.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","committed_date":"2015-11-13T05:23:14.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","committed_date":"2015-11-13T05:08:45.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","committed_date":"2015-11-13T05:08:04.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","committed_date":"2015-08-25T17:53:12.000+02:00","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":14,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","committed_date":"2015-01-10T22:23:29.000+01:00","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":14,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","committed_date":"2015-01-10T21:28:18.000+01:00","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}},{"merge_request_diff_id":14,"relative_order":15,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","committed_date":"2014-02-27T10:01:38.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":16,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","committed_date":"2014-02-27T09:57:31.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":17,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","committed_date":"2014-02-27T09:54:21.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":18,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","committed_date":"2014-02-27T09:49:50.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":19,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","committed_date":"2014-02-27T09:48:32.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":14,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":14,"created_at":"2016-06-14T15:02:24.770Z","updated_at":"2016-06-14T15:02:25.007Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":224,"target_type":"MergeRequest","target_id":14,"project_id":36,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":1},{"id":174,"target_type":"MergeRequest","target_id":14,"project_id":5,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":20}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group/project.json b/spec/fixtures/lib/gitlab/import_export/group/project.json
index e8e1e53a86a..671ff92087b 100644
--- a/spec/fixtures/lib/gitlab/import_export/group/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/group/project.json
@@ -205,6 +205,18 @@
"iid": 1,
"group_id": 100
},
+ "iteration": {
+ "created_at": "2022-08-15T12:55:42.607Z",
+ "updated_at": "2022-08-15T12:56:19.269Z",
+ "start_date": "2022-08-15",
+ "due_date": "2022-08-21",
+ "group_id": 260,
+ "iid": 5,
+ "description": "iteration description",
+ "iterations_cadence": {
+ "title": "iterations cadence"
+ }
+ },
"epic_issue": {
"id": 78,
"relative_position": 1073740323,
@@ -239,7 +251,26 @@
"due_date_sourcing_epic_id": null,
"milestone_id": null
}
- }
+ },
+ "resource_iteration_events": [
+ {
+ "user_id": 1,
+ "created_at": "2022-08-17T13:04:02.495Z",
+ "action": "add",
+ "iteration": {
+ "created_at": "2022-08-15T12:55:42.607Z",
+ "updated_at": "2022-08-15T12:56:19.269Z",
+ "start_date": "2022-08-15",
+ "due_date": "2022-08-21",
+ "group_id": 260,
+ "iid": 5,
+ "description": "iteration description",
+ "iterations_cadence": {
+ "title": "iterations cadence"
+ }
+ }
+ }
+ ]
}
],
"snippets": [
diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson
index 4759e97228f..9596986dca0 100644
--- a/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson
@@ -1,3 +1,3 @@
{"id":1,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"opened","iid":1,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":1,"title":"Project milestone","project_id":8,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null},"label_links":[{"id":11,"label_id":6,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"group label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"GroupLabel","priorities":[]}},{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"A project label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"ProjectLabel","priorities":[]}}]}
{"id":2,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"closed","iid":2,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"label_links":[{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":2,"title":"A project label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"ProjectLabel","priorities":[]}}]}
-{"id":3,"title":"Issue with Epic","author_id":1,"project_id":8,"created_at":"2019-12-08T19:41:11.233Z","updated_at":"2019-12-08T19:41:53.194Z","position":0,"branch_name":null,"description":"Donec at nulla vitae sem molestie rutrum ut at sem.","state":"opened","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"notes":[],"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"epic_issue":{"id":78,"relative_position":1073740323,"epic":{"id":1,"group_id":5,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-12-08T19:37:07.098Z","updated_at":"2019-12-08T19:43:11.568Z","title":"An epic","description":null,"start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"milestone_id":null}}}
+{"id":3,"title":"Issue with Epic","author_id":1,"project_id":8,"created_at":"2019-12-08T19:41:11.233Z","updated_at":"2019-12-08T19:41:53.194Z","position":0,"branch_name":null,"description":"Donec at nulla vitae sem molestie rutrum ut at sem.","state":"opened","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"notes":[],"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"iteration":{"created_at":"2022-08-15T12:55:42.607Z","updated_at":"2022-08-15T12:56:19.269Z","start_date":"2022-08-15","due_date":"2022-08-21","group_id":260,"iid":5,"description":"iteration description","iterations_cadence":{"title":"iterations cadence"}},"epic_issue":{"id":78,"relative_position":1073740323,"epic":{"id":1,"group_id":5,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-12-08T19:37:07.098Z","updated_at":"2019-12-08T19:43:11.568Z","title":"An epic","description":null,"start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"milestone_id":null}},"resource_iteration_events":[{"user_id":1,"created_at":"2022-08-17T13:04:02.495Z","action":"add","iteration":{"created_at":"2022-08-15T12:55:42.607Z","updated_at":"2022-08-15T12:56:19.269Z","start_date":"2022-08-15","due_date":"2022-08-21","group_id":260,"iid":5,"description":"iteration description","iterations_cadence":{"title":"iterations cadence"}}}]}
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 18cd63b7bcb..14885813d93 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -283,7 +283,23 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [x] Complete sub-task 1
- [X] Complete task 2
-#### Gollum Tags
+### Math
+
+- Dollar math: $a^2 + b^2 = c^2$
+- Dollar math and snippet reference: $d^2 + e^2 = f^2$ and <%= snippet.to_reference %>
+- Dollar math and snippet in another project: <%= xsnippet.to_reference(project) %> and $g^2 + h^2 = i^2$
+- Not dollar math: $20,000 and $30,000
+- Dollar-backtick math: $`j^2 + k^2 = l^2`$
+- Dollar display math: $$m^2 + n^2 = o^2$$
+- Dollar display math and snippet reference: $$p^2 + q^2 = r^2$$ and <%= snippet.to_reference %>
+- Dollar math and snippet in another project: <%= xsnippet.to_reference(project) %> and $$s^2 + t^2 = u^2$$
+- Display math using a block
+
+ ```math
+ v^2 + w^2 = x^2
+ ```
+
+### Gollum Tags
- [[linked-resource]]
- [[link-text|linked-resource]]
@@ -326,15 +342,15 @@ However the wrapping tags cannot be mixed as such:
### Colors
-`#F00`
-`#F00A`
-`#FF0000`
-`#FF0000AA`
-`RGB(0,255,0)`
-`RGB(0%,100%,0%)`
-`RGBA(0,255,0,0.7)`
-`HSL(540,70%,50%)`
-`HSLA(540,70%,50%,0.7)`
+`#F00`
+`#F00A`
+`#FF0000`
+`#FF0000AA`
+`RGB(0,255,0)`
+`RGB(0%,100%,0%)`
+`RGBA(0,255,0,0.7)`
+`HSL(540,70%,50%)`
+`HSLA(540,70%,50%,0.7)`
### Mermaid
diff --git a/spec/fixtures/markdown/markdown_golden_master_examples.yml b/spec/fixtures/markdown/markdown_golden_master_examples.yml
index a1ad88ef69c..495d00026d7 100644
--- a/spec/fixtures/markdown/markdown_golden_master_examples.yml
+++ b/spec/fixtures/markdown/markdown_golden_master_examples.yml
@@ -773,7 +773,7 @@
markdown: |-
Hi @gfm_user - thank you for reporting this bug (#1) we hope to fix it in %1.1 as part of !1
html: |-
- <p data-sourcepos="1:1-1:92" dir="auto">Hi <a href="/gfm_user" data-user="1" data-reference-type="user" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="John Doe1">@gfm_user</a> - thank you for reporting this bug (<a href="/group1/project1/-/issues/1" data-original="#1" data-link="false" data-link-reference="false" data-project="11" data-issue="11" data-project-path="group1/project1" data-iid="1" data-issue-type="issue" data-reference-type="issue" data-container="body" data-placement="top" title="My title 1" class="gfm gfm-issue">#1</a>) we hope to fix it in <a href="/group1/project1/-/milestones/1" data-original="%1.1" data-link="false" data-link-reference="false" data-project="11" data-milestone="11" data-reference-type="milestone" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%1.1</a> as part of <a href="/group1/project1/-/merge_requests/1" data-original="!1" data-link="false" data-link-reference="false" data-project="11" data-merge-request="11" data-project-path="group1/project1" data-iid="1" data-reference-type="merge_request" data-container="body" data-placement="top" title="My title 2" class="gfm gfm-merge_request">!1</a></p>
+ <p data-sourcepos="1:1-1:92" dir="auto">Hi <a href="/gfm_user" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="John Doe1">@gfm_user</a> - thank you for reporting this bug (<a href="/group1/project1/-/issues/1" data-reference-type="issue" data-original="#1" data-link="false" data-link-reference="false" data-project="11" data-issue="11" data-project-path="group1/project1" data-iid="1" data-issue-type="issue" data-container="body" data-placement="top" title="My title 1" class="gfm gfm-issue">#1</a>) we hope to fix it in <a href="/group1/project1/-/milestones/1" data-reference-type="milestone" data-original="%1.1" data-link="false" data-link-reference="false" data-project="11" data-milestone="11" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%1.1</a> as part of <a href="/group1/project1/-/merge_requests/1" data-reference-type="merge_request" data-original="!1" data-link="false" data-link-reference="false" data-project="11" data-merge-request="11" data-project-path="group1/project1" data-iid="1" data-container="body" data-placement="top" title="My title 2" class="gfm gfm-merge_request">!1</a></p>
- name: strike
markdown: |-
~~del~~
diff --git a/spec/fixtures/packages/debian/distribution/D-I-Packages b/spec/fixtures/packages/debian/distribution/D-I-Packages
new file mode 100644
index 00000000000..80272e3a12c
--- /dev/null
+++ b/spec/fixtures/packages/debian/distribution/D-I-Packages
@@ -0,0 +1,2 @@
+Package: example-package
+Description: This is an incomplete D-I Packages file
diff --git a/spec/fixtures/packages/debian/distribution/OtherSHA256 b/spec/fixtures/packages/debian/distribution/OtherSHA256
new file mode 100644
index 00000000000..5c282d72c11
--- /dev/null
+++ b/spec/fixtures/packages/debian/distribution/OtherSHA256
@@ -0,0 +1 @@
+Other SHA256 \ No newline at end of file
diff --git a/spec/fixtures/packages/debian/distribution/Sources b/spec/fixtures/packages/debian/distribution/Sources
new file mode 100644
index 00000000000..1097f1b1aff
--- /dev/null
+++ b/spec/fixtures/packages/debian/distribution/Sources
@@ -0,0 +1,2 @@
+Package: example-package
+Description: This is an incomplete Sources file
diff --git a/spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm b/spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm
new file mode 100644
index 00000000000..bff3193a9e8
--- /dev/null
+++ b/spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm
Binary files differ
diff --git a/spec/fixtures/security_reports/deprecated/gl-sast-report.json b/spec/fixtures/security_reports/deprecated/gl-sast-report.json
index 2f7e47281e2..c5b0148fe3e 100644
--- a/spec/fixtures/security_reports/deprecated/gl-sast-report.json
+++ b/spec/fixtures/security_reports/deprecated/gl-sast-report.json
@@ -961,4 +961,4 @@
"url": "https://cwe.mitre.org/data/definitions/120.html",
"tool": "flawfinder"
}
-]
+] \ No newline at end of file
diff --git a/spec/fixtures/security_reports/feature-branch/gl-sast-report.json b/spec/fixtures/security_reports/feature-branch/gl-sast-report.json
index f93233e0ebb..51761583c70 100644
--- a/spec/fixtures/security_reports/feature-branch/gl-sast-report.json
+++ b/spec/fixtures/security_reports/feature-branch/gl-sast-report.json
@@ -174,4 +174,4 @@
"start_time": "placeholder-value",
"end_time": "placeholder-value"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json b/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json
index 538364f84a2..4862a504cec 100644
--- a/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json
+++ b/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json
@@ -2,4 +2,4 @@
"version": "14.1.2",
"vulnerabilities": [],
"remediations": []
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json b/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json
index 3cfb3e51ef7..ef2ff7443d3 100644
--- a/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json
+++ b/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json
@@ -165,4 +165,4 @@
"end_time": "placeholder-value",
"status": "success"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json b/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json
new file mode 100644
index 00000000000..417dc960aff
--- /dev/null
+++ b/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json
@@ -0,0 +1,50 @@
+{
+ "vulnerabilities": [
+ {
+ "category": "dependency_scanning",
+ "name": "Vulnerability for remediation testing 1",
+ "message": "This vulnerability should have ONE remediation",
+ "description": "",
+ "cve": "CVE-2137",
+ "severity": "High",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {},
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Foo vulnerability",
+ "value": "foo"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137"
+ }
+ ],
+ "details": {
+ "commit": {
+ "name": [
+ {
+ "lang": "en",
+ "value": "The Commit"
+ }
+ ],
+ "description": [
+ {
+ "lang": "en",
+ "value": "Commit where the vulnerability was identified"
+ }
+ ],
+ "type": "commit",
+ "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19"
+ }
+ }
+ }
+ ],
+ "dependency_files": [],
+ "version": "14.0.2"
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report.json b/spec/fixtures/security_reports/master/gl-common-scanning-report.json
index 787573301bb..1295b44d4df 100644
--- a/spec/fixtures/security_reports/master/gl-common-scanning-report.json
+++ b/spec/fixtures/security_reports/master/gl-common-scanning-report.json
@@ -1,5 +1,6 @@
{
- "vulnerabilities": [{
+ "vulnerabilities": [
+ {
"category": "dependency_scanning",
"name": "Vulnerability for remediation testing 1",
"message": "This vulnerability should have ONE remediation",
@@ -12,24 +13,32 @@
"name": "Gemnasium"
},
"location": {},
- "identifiers": [{
- "type": "GitLab",
- "name": "Foo vulnerability",
- "value": "foo"
- }],
- "links": [{
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137"
- }],
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Foo vulnerability",
+ "value": "foo"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137"
+ }
+ ],
"details": {
"commit": {
- "name": [{
- "lang": "en",
- "value": "The Commit"
- }],
- "description": [{
- "lang": "en",
- "value": "Commit where the vulnerability was identified"
- }],
+ "name": [
+ {
+ "lang": "en",
+ "value": "The Commit"
+ }
+ ],
+ "description": [
+ {
+ "lang": "en",
+ "value": "Commit where the vulnerability was identified"
+ }
+ ],
"type": "commit",
"value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19"
}
@@ -48,24 +57,32 @@
"name": "Gemnasium"
},
"location": {},
- "identifiers": [{
- "type": "GitLab",
- "name": "Foo vulnerability",
- "value": "foo"
- }],
- "links": [{
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2138"
- }],
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Foo vulnerability",
+ "value": "foo"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2138"
+ }
+ ],
"details": {
"commit": {
- "name": [{
- "lang": "en",
- "value": "The Commit"
- }],
- "description": [{
- "lang": "en",
- "value": "Commit where the vulnerability was identified"
- }],
+ "name": [
+ {
+ "lang": "en",
+ "value": "The Commit"
+ }
+ ],
+ "description": [
+ {
+ "lang": "en",
+ "value": "Commit where the vulnerability was identified"
+ }
+ ],
"type": "commit",
"value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19"
}
@@ -84,24 +101,32 @@
"name": "Gemnasium"
},
"location": {},
- "identifiers": [{
- "type": "GitLab",
- "name": "Foo vulnerability",
- "value": "foo"
- }],
- "links": [{
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2139"
- }],
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Foo vulnerability",
+ "value": "foo"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2139"
+ }
+ ],
"details": {
"commit": {
- "name": [{
- "lang": "en",
- "value": "The Commit"
- }],
- "description": [{
- "lang": "en",
- "value": "Commit where the vulnerability was identified"
- }],
+ "name": [
+ {
+ "lang": "en",
+ "value": "The Commit"
+ }
+ ],
+ "description": [
+ {
+ "lang": "en",
+ "value": "Commit where the vulnerability was identified"
+ }
+ ],
"type": "commit",
"value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19"
}
@@ -120,24 +145,32 @@
"name": "Gemnasium"
},
"location": {},
- "identifiers": [{
- "type": "GitLab",
- "name": "Foo vulnerability",
- "value": "foo"
- }],
- "links": [{
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2140"
- }],
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Foo vulnerability",
+ "value": "foo"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2140"
+ }
+ ],
"details": {
"commit": {
- "name": [{
- "lang": "en",
- "value": "The Commit"
- }],
- "description": [{
- "lang": "en",
- "value": "Commit where the vulnerability was identified"
- }],
+ "name": [
+ {
+ "lang": "en",
+ "value": "The Commit"
+ }
+ ],
+ "description": [
+ {
+ "lang": "en",
+ "value": "Commit where the vulnerability was identified"
+ }
+ ],
"type": "commit",
"value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19"
}
@@ -162,30 +195,37 @@
},
"summary": "The Origin header was changed to an invalid value of http://peachapisecurity.com and the response contained an Access-Control-Allow-Origin header which included this invalid Origin, indicating that the CORS configuration on the server is overly permissive.\n\n\n",
"request": {
- "headers": [{
- "name": "Host",
- "value": "127.0.0.1:7777"
- }],
+ "headers": [
+ {
+ "name": "Host",
+ "value": "127.0.0.1:7777"
+ }
+ ],
"method": "GET",
"url": "http://127.0.0.1:7777/api/users",
"body": ""
},
"response": {
- "headers": [{
- "name": "Server",
- "value": "TwistedWeb/20.3.0"
- }],
+ "headers": [
+ {
+ "name": "Server",
+ "value": "TwistedWeb/20.3.0"
+ }
+ ],
"reason_phrase": "OK",
"status_code": 200,
"body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]"
},
- "supporting_messages": [{
+ "supporting_messages": [
+ {
"name": "Origional",
"request": {
- "headers": [{
- "name": "Host",
- "value": "127.0.0.1:7777"
- }],
+ "headers": [
+ {
+ "name": "Host",
+ "value": "127.0.0.1:7777"
+ }
+ ],
"method": "GET",
"url": "http://127.0.0.1:7777/api/users",
"body": ""
@@ -194,19 +234,23 @@
{
"name": "Recorded",
"request": {
- "headers": [{
- "name": "Host",
- "value": "127.0.0.1:7777"
- }],
+ "headers": [
+ {
+ "name": "Host",
+ "value": "127.0.0.1:7777"
+ }
+ ],
"method": "GET",
"url": "http://127.0.0.1:7777/api/users",
"body": ""
},
"response": {
- "headers": [{
- "name": "Server",
- "value": "TwistedWeb/20.3.0"
- }],
+ "headers": [
+ {
+ "name": "Server",
+ "value": "TwistedWeb/20.3.0"
+ }
+ ],
"reason_phrase": "OK",
"status_code": 200,
"body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]"
@@ -215,24 +259,32 @@
]
},
"location": {},
- "identifiers": [{
- "type": "GitLab",
- "name": "Foo vulnerability",
- "value": "foo"
- }],
- "links": [{
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020"
- }],
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Foo vulnerability",
+ "value": "foo"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020"
+ }
+ ],
"details": {
"commit": {
- "name": [{
- "lang": "en",
- "value": "The Commit"
- }],
- "description": [{
- "lang": "en",
- "value": "Commit where the vulnerability was identified"
- }],
+ "name": [
+ {
+ "lang": "en",
+ "value": "The Commit"
+ }
+ ],
+ "description": [
+ {
+ "lang": "en",
+ "value": "Commit where the vulnerability was identified"
+ }
+ ],
"type": "commit",
"value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19"
}
@@ -258,30 +310,37 @@
},
"summary": "The Origin header was changed to an invalid value of http://peachapisecurity.com and the response contained an Access-Control-Allow-Origin header which included this invalid Origin, indicating that the CORS configuration on the server is overly permissive.\n\n\n",
"request": {
- "headers": [{
- "name": "Host",
- "value": "127.0.0.1:7777"
- }],
+ "headers": [
+ {
+ "name": "Host",
+ "value": "127.0.0.1:7777"
+ }
+ ],
"method": "GET",
"url": "http://127.0.0.1:7777/api/users",
"body": ""
},
"response": {
- "headers": [{
- "name": "Server",
- "value": "TwistedWeb/20.3.0"
- }],
+ "headers": [
+ {
+ "name": "Server",
+ "value": "TwistedWeb/20.3.0"
+ }
+ ],
"reason_phrase": "OK",
"status_code": 200,
"body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]"
},
- "supporting_messages": [{
+ "supporting_messages": [
+ {
"name": "Origional",
"request": {
- "headers": [{
- "name": "Host",
- "value": "127.0.0.1:7777"
- }],
+ "headers": [
+ {
+ "name": "Host",
+ "value": "127.0.0.1:7777"
+ }
+ ],
"method": "GET",
"url": "http://127.0.0.1:7777/api/users",
"body": ""
@@ -290,19 +349,23 @@
{
"name": "Recorded",
"request": {
- "headers": [{
- "name": "Host",
- "value": "127.0.0.1:7777"
- }],
+ "headers": [
+ {
+ "name": "Host",
+ "value": "127.0.0.1:7777"
+ }
+ ],
"method": "GET",
"url": "http://127.0.0.1:7777/api/users",
"body": ""
},
"response": {
- "headers": [{
- "name": "Server",
- "value": "TwistedWeb/20.3.0"
- }],
+ "headers": [
+ {
+ "name": "Server",
+ "value": "TwistedWeb/20.3.0"
+ }
+ ],
"reason_phrase": "OK",
"status_code": 200,
"body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]"
@@ -311,15 +374,19 @@
]
},
"location": {},
- "identifiers": [{
- "type": "GitLab",
- "name": "Bar vulnerability",
- "value": "bar"
- }],
- "links": [{
- "name": "CVE-1030",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030"
- }]
+ "identifiers": [
+ {
+ "type": "GitLab",
+ "name": "Bar vulnerability",
+ "value": "bar"
+ }
+ ],
+ "links": [
+ {
+ "name": "CVE-1030",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030"
+ }
+ ]
},
{
"category": "dependency_scanning",
@@ -338,57 +405,73 @@
"links": []
}
],
- "remediations": [{
- "fixes": [{
- "cve": "CVE-2137"
- }],
+ "remediations": [
+ {
+ "fixes": [
+ {
+ "cve": "CVE-2137"
+ }
+ ],
"summary": "this remediates CVE-2137",
"diff": "dG90YWxseSBsZWdpdCBkaWZm"
},
{
- "fixes": [{
- "cve": "CVE-2138"
- }],
+ "fixes": [
+ {
+ "cve": "CVE-2138"
+ }
+ ],
"summary": "this remediates CVE-2138",
"diff": "dG90YWxseSBsZWdpdCBkaWZm"
},
{
- "fixes": [{
- "cve": "CVE-2139"
- }, {
- "cve": "CVE-2140"
- }],
+ "fixes": [
+ {
+ "cve": "CVE-2139"
+ },
+ {
+ "cve": "CVE-2140"
+ }
+ ],
"summary": "this remediates CVE-2139 and CVE-2140",
"diff": "dG90YWxseSBsZWdpdGltYXRlIGRpZmYsIDEwLzEwIHdvdWxkIGFwcGx5"
},
{
- "fixes": [{
- "cve": "CVE-1020"
- }],
+ "fixes": [
+ {
+ "cve": "CVE-1020"
+ }
+ ],
"summary": "",
"diff": ""
},
{
- "fixes": [{
- "cve": "CVE",
- "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3"
- }],
+ "fixes": [
+ {
+ "cve": "CVE",
+ "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3"
+ }
+ ],
"summary": "",
"diff": ""
},
{
- "fixes": [{
- "cve": "CVE",
- "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3"
- }],
+ "fixes": [
+ {
+ "cve": "CVE",
+ "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3"
+ }
+ ],
"summary": "",
"diff": ""
},
{
- "fixes": [{
- "id": "2134",
- "cve": "CVE-1"
- }],
+ "fixes": [
+ {
+ "id": "2134",
+ "cve": "CVE-1"
+ }
+ ],
"summary": "",
"diff": ""
}
@@ -406,7 +489,7 @@
},
"scanner": {
"id": "gemnasium",
- "name": "Gemnasium",
+ "name": "Gemnasium top-level",
"url": "https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven",
"vendor": {
"name": "GitLab"
@@ -419,4 +502,4 @@
"status": "success"
},
"version": "14.0.2"
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json b/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json
index ab3ee348263..fcfd9b831f4 100644
--- a/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json
+++ b/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json
@@ -799,4 +799,4 @@
"url": "https://cwe.mitre.org/data/definitions/120.html"
}
]
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-report-bandit.json b/spec/fixtures/security_reports/master/gl-sast-report-bandit.json
index a80833354ed..d0346479b85 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report-bandit.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report-bandit.json
@@ -40,4 +40,4 @@
"end_time": "2022-03-11T00:21:50",
"status": "success"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-report-gosec.json b/spec/fixtures/security_reports/master/gl-sast-report-gosec.json
index 42986ea1045..4c385326c8c 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report-gosec.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report-gosec.json
@@ -65,4 +65,4 @@
"end_time": "2022-03-15T20:33:17",
"status": "success"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-report-minimal.json b/spec/fixtures/security_reports/master/gl-sast-report-minimal.json
index 60a67453c9b..5e9273d43b1 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report-minimal.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report-minimal.json
@@ -65,4 +65,4 @@
"start_time": "placeholder-value",
"end_time": "placeholder-value"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json
index 2a60a75366e..037b9fb8d3e 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json
@@ -68,4 +68,4 @@
"end_time": "2022-03-11T18:48:22",
"status": "success"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json
index 3d8c65d5823..f01d26a69c9 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json
@@ -67,4 +67,4 @@
"end_time": "2022-03-15T20:37:05",
"status": "success"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-sast-report.json b/spec/fixtures/security_reports/master/gl-sast-report.json
index 63504e6fccc..1aa8db1a65f 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report.json
@@ -197,4 +197,4 @@
"start_time": "placeholder-value",
"end_time": "placeholder-value"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/security_reports/master/gl-secret-detection-report.json b/spec/fixtures/security_reports/master/gl-secret-detection-report.json
index 9b0b2a19beb..21d4f3f1798 100644
--- a/spec/fixtures/security_reports/master/gl-secret-detection-report.json
+++ b/spec/fixtures/security_reports/master/gl-secret-detection-report.json
@@ -30,4 +30,4 @@
}
],
"remediations": []
-}
+} \ No newline at end of file
diff --git a/spec/frontend/__helpers__/datetime_helpers.js b/spec/frontend/__helpers__/datetime_helpers.js
index 25dbd1d477d..cbe627b7968 100644
--- a/spec/frontend/__helpers__/datetime_helpers.js
+++ b/spec/frontend/__helpers__/datetime_helpers.js
@@ -1,4 +1,4 @@
-import dateFormat from 'dateformat';
+import dateFormat from '~/lib/dateformat';
/**
* Returns a date object corresponding to the given date string.
diff --git a/spec/frontend/__helpers__/dl_locator_helper.js b/spec/frontend/__helpers__/dl_locator_helper.js
index b507dcd599d..591c034be9b 100644
--- a/spec/frontend/__helpers__/dl_locator_helper.js
+++ b/spec/frontend/__helpers__/dl_locator_helper.js
@@ -19,10 +19,13 @@ import { createWrapper, ErrorWrapper } from '@vue/test-utils';
* @returns Wrapper
*/
export const findDd = (dtLabel, wrapper) => {
- const dt = wrapper.findByText(dtLabel).element;
- const dd = dt.nextElementSibling;
- if (dt.tagName === 'DT' && dd.tagName === 'DD') {
- return createWrapper(dd, {});
+ const dtw = wrapper.findByText(dtLabel);
+ if (dtw.exists()) {
+ const dt = dtw.element;
+ const dd = dt.nextElementSibling;
+ if (dt.tagName === 'DT' && dd.tagName === 'DD') {
+ return createWrapper(dd, {});
+ }
}
- return ErrorWrapper(dtLabel);
+ return new ErrorWrapper(dtLabel);
};
diff --git a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
index dcccc14f396..54d397d0997 100644
--- a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
+++ b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
@@ -17,16 +17,16 @@ describe('keepAlive', () => {
});
it('converts a component to a keep-alive component', async () => {
- const { element } = wrapper.find(component);
+ const { element } = wrapper.findComponent(component);
await wrapper.vm.deactivate();
- expect(wrapper.find(component).exists()).toBe(false);
+ expect(wrapper.findComponent(component).exists()).toBe(false);
await wrapper.vm.activate();
// assert that when the component is destroyed and re-rendered, the
// newly rendered component has the reference to the old component
// (i.e. the old component was deactivated and activated)
- expect(wrapper.find(component).element).toBe(element);
+ expect(wrapper.findComponent(component).element).toBe(element);
});
});
diff --git a/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
index fd42c710c65..e6096221528 100644
--- a/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
+++ b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
@@ -38,7 +38,7 @@ describe('custom matcher toValidateJsonSchema', () => {
});
it('throws if not matching', () => {
- expect(() => expect(null).toValidateJsonSchema(schema)).toThrowError(
+ expect(() => expect(null).toValidateJsonSchema(schema)).toThrow(
`Expected the given data to pass the schema validation, but found that it was considered invalid. Errors:
Error with item : must be object`,
);
@@ -57,7 +57,7 @@ Error with item : must be object`,
});
it('throws if matching', () => {
- expect(() => expect({ fruit: 'apple' }).not.toValidateJsonSchema(schema)).toThrowError(
+ expect(() => expect({ fruit: 'apple' }).not.toValidateJsonSchema(schema)).toThrow(
'Expected the given data not to pass the schema validation, but found that it was considered valid.',
);
});
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 4d6486544ca..45a7b8e0352 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -48,9 +48,6 @@ testUtilsConfig.deprecationWarningHandler = (method, message) => {
const ALLOWED_DEPRECATED_METHODS = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/295679
'finding components with `find` or `get`',
-
- // https://gitlab.com/gitlab-org/gitlab/-/issues/295680
- 'finding components with `findAll`',
];
if (!ALLOWED_DEPRECATED_METHODS.includes(method)) {
global.console.error(message);
diff --git a/spec/frontend/__mocks__/sortablejs/index.js b/spec/frontend/__mocks__/sortablejs/index.js
index 5039af54542..d8bc8ae9bda 100644
--- a/spec/frontend/__mocks__/sortablejs/index.js
+++ b/spec/frontend/__mocks__/sortablejs/index.js
@@ -1,4 +1,4 @@
-const Sortablejs = jest.genMockFromModule('sortablejs');
+const Sortablejs = jest.createMockFromModule('sortablejs');
export default Sortablejs;
export const Sortable = Sortablejs;
diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
index 6013fa3ec39..aed3db4aa4c 100644
--- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js
+++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
@@ -190,6 +190,21 @@ describe('~/access_tokens/components/access_token_table_app', () => {
expect(button.props('category')).toBe('tertiary');
});
+ describe('revoke path', () => {
+ beforeEach(() => {
+ createComponent({ showRole: true });
+ });
+
+ it.each([{ revoke_path: null }, { revoke_path: undefined }])(
+ 'with %p, does not show revoke button',
+ async (input) => {
+ await triggerSuccess(defaultActiveAccessTokens.map((data) => ({ ...data, ...input })));
+
+ expect(findCells().at(6).findComponent(GlButton).exists()).toBe(false);
+ },
+ );
+ });
+
it('sorts rows alphabetically', async () => {
createComponent({ showRole: true });
await triggerSuccess();
diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js
index 646dc0d703f..491d2a0e323 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -58,4 +58,20 @@ describe('~/access_tokens/components/expires_at_field', () => {
expect(findDatepicker().props('defaultDate')).toStrictEqual(future);
});
+
+ it('should set the default expiration date to be 365 days', () => {
+ const offset = 365;
+ const today = new Date();
+ const future = getDateInFuture(today, offset);
+ createComponent({ defaultDateOffset: offset });
+
+ expect(findDatepicker().props('defaultDate')).toStrictEqual(future);
+ });
+
+ it('should set the default expiration date to maxDate, ignoring defaultDateOffset', () => {
+ const maxDate = new Date();
+ createComponent({ maxDate, defaultDateOffset: 2 });
+
+ expect(findDatepicker().props('defaultDate')).toStrictEqual(maxDate);
+ });
});
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index 9ccadbebf7a..d12d200d214 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -23,18 +23,27 @@ describe('~/access_tokens/components/new_access_token_app', () => {
};
const triggerSuccess = async (newToken = 'new token') => {
- wrapper.find(DomElementListener).vm.$emit(EVENT_SUCCESS, { detail: [{ new_token: newToken }] });
+ wrapper
+ .findComponent(DomElementListener)
+ .vm.$emit(EVENT_SUCCESS, { detail: [{ new_token: newToken }] });
await nextTick();
};
const triggerError = async (errors = ['1', '2']) => {
- wrapper.find(DomElementListener).vm.$emit(EVENT_ERROR, { detail: [{ errors }] });
+ wrapper.findComponent(DomElementListener).vm.$emit(EVENT_ERROR, { detail: [{ errors }] });
await nextTick();
};
beforeEach(() => {
// NewAccessTokenApp observes a form element
- setHTMLFixture(`<form id="${FORM_SELECTOR.slice(1)}"><input type="submit"/></form>`);
+ setHTMLFixture(
+ `<form id="${FORM_SELECTOR.slice(1)}">
+ <input type="text" id="expires_at" value="2022-01-01"/>
+ <input type="text" value='1'/>
+ <input type="checkbox" checked/>
+ <input type="submit" value="Create"/>
+ </form>`,
+ );
createComponent();
});
@@ -78,7 +87,6 @@ describe('~/access_tokens/components/new_access_token_app', () => {
.findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType }))
.attributes();
expect(inputAttributes).toMatchObject({
- class: expect.stringContaining('qa-created-access-token'),
'data-qa-selector': 'created_access_token_field',
});
});
@@ -94,12 +102,29 @@ describe('~/access_tokens/components/new_access_token_app', () => {
});
});
- it('should reset the form', async () => {
- const resetSpy = jest.spyOn(wrapper.vm.form, 'reset');
+ describe('when resetting the form', () => {
+ it('should reset selectively some input fields', async () => {
+ expect(document.querySelector('input[type=text]:not([id$=expires_at])').value).toBe('1');
+ expect(document.querySelector('input[type=checkbox]').checked).toBe(true);
+ await triggerSuccess();
- await triggerSuccess();
+ expect(document.querySelector('input[type=text]:not([id$=expires_at])').value).toBe('');
+ expect(document.querySelector('input[type=checkbox]').checked).toBe(false);
+ });
- expect(resetSpy).toHaveBeenCalled();
+ it('should not reset the date field', async () => {
+ expect(document.querySelector('input[type=text][id$=expires_at]').value).toBe('2022-01-01');
+ await triggerSuccess();
+
+ expect(document.querySelector('input[type=text][id$=expires_at]').value).toBe('2022-01-01');
+ });
+
+ it('should not reset the submit button value', async () => {
+ expect(document.querySelector('input[type=submit]').value).toBe('Create');
+ await triggerSuccess();
+
+ expect(document.querySelector('input[type=submit]').value).toBe('Create');
+ });
});
});
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index 0c611a4a512..55575ab25fc 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -182,7 +182,7 @@ describe('access tokens', () => {
});
describe('initTokensApp', () => {
- it('mounts the component and provides`tokenTypes` ', () => {
+ it('mounts the component and provides`tokenTypes`', () => {
const tokensData = {
[FEED_TOKEN]: FEED_TOKEN,
[INCOMING_EMAIL_TOKEN]: INCOMING_EMAIL_TOKEN,
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index bffadbde087..1d57473943b 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -48,8 +48,8 @@ describe('AddContextCommitsModal', () => {
return wrapper;
};
- const findModal = () => wrapper.find(GlModal);
- const findSearch = () => wrapper.find(GlSearchBoxByType);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
beforeEach(() => {
wrapper = createWrapper();
@@ -75,7 +75,7 @@ describe('AddContextCommitsModal', () => {
it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => {
const searchText = 'abcd';
findSearch().vm.$emit('input', searchText);
- expect(searchCommits).not.toBeCalled();
+ expect(searchCommits).not.toHaveBeenCalled();
jest.advanceTimersByTime(500);
expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText);
});
@@ -107,12 +107,12 @@ describe('AddContextCommitsModal', () => {
it('a disabled ok button in first tab, when row is selected in second tab', () => {
createWrapper({ selectedContextCommits: [commit] });
- expect(wrapper.find(GlModal).attributes('ok-disabled')).toBe('true');
+ expect(wrapper.findComponent(GlModal).attributes('ok-disabled')).toBe('true');
});
});
describe('has an ok button when clicked calls action', () => {
- it('"createContextCommits" when only new commits to be added ', async () => {
+ it('"createContextCommits" when only new commits to be added', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
await nextTick();
@@ -121,7 +121,7 @@ describe('AddContextCommitsModal', () => {
forceReload: true,
});
});
- it('"removeContextCommits" when only added commits are to be removed ', async () => {
+ it('"removeContextCommits" when only added commits are to be removed', async () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
await nextTick();
diff --git a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
index 85ecb4313c2..f679576182f 100644
--- a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
@@ -32,7 +32,7 @@ describe('ReviewTabContainer', () => {
it('shows loading icon when commits are being loaded', () => {
createWrapper({ isLoading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('shows loading error text when API call fails', () => {
@@ -46,6 +46,6 @@ describe('ReviewTabContainer', () => {
it('renders all passed commits as list', () => {
createWrapper({ commits: [commit] });
- expect(wrapper.findAll(CommitItem).length).toBe(1);
+ expect(wrapper.findAllComponents(CommitItem).length).toBe(1);
});
});
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
index 534af2a3033..de56e843eb9 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
@@ -34,7 +34,7 @@ describe('DevopsScore', () => {
createComponent({ devopsScoreMetrics: {} });
});
- it('includes the DevopsScoreCallout component ', () => {
+ it('includes the DevopsScoreCallout component', () => {
expect(bannerExists()).toBe(true);
});
@@ -67,7 +67,7 @@ describe('DevopsScore', () => {
createComponent();
});
- it('includes the DevopsScoreCallout component ', () => {
+ it('includes the DevopsScoreCallout component', () => {
expect(bannerExists()).toBe(true);
});
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
new file mode 100644
index 00000000000..f61af6203f0
--- /dev/null
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -0,0 +1,91 @@
+import { GlAvatarLabeled, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TopicSelect from '~/admin/topics/components/topic_select.vue';
+
+const mockTopics = [
+ { id: 1, name: 'topic1', title: 'Topic 1', avatarUrl: 'avatar.com/topic1.png' },
+ { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
+];
+
+describe('TopicSelect', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
+ function createComponent(props = {}) {
+ wrapper = shallowMount(TopicSelect, {
+ propsData: props,
+ data() {
+ return {
+ topics: mockTopics,
+ search: '',
+ };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ topics: { loading: false },
+ },
+ },
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('mounts', () => {
+ createComponent();
+
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('`selectedTopic` prop defaults to `{}`', () => {
+ createComponent();
+
+ expect(wrapper.props('selectedTopic')).toEqual({});
+ });
+
+ it('`labelText` prop defaults to `null`', () => {
+ createComponent();
+
+ expect(wrapper.props('labelText')).toBe(null);
+ });
+
+ it('renders default text if no selected topic', () => {
+ createComponent();
+
+ expect(findDropdown().props('text')).toBe('Select a topic');
+ });
+
+ it('renders selected topic', () => {
+ createComponent({ selectedTopic: mockTopics[0] });
+
+ expect(findDropdown().props('text')).toBe('topic1');
+ });
+
+ it('renders label', () => {
+ createComponent({ labelText: 'my label' });
+
+ expect(wrapper.find('label').text()).toBe('my label');
+ });
+
+ it('renders dropdown items', () => {
+ createComponent();
+
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1');
+ expect(dropdownItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab');
+ });
+
+ it('emits `click` event when topic selected', () => {
+ createComponent();
+
+ findAllDropdownItems().at(0).vm.$emit('click');
+
+ expect(wrapper.emitted('click')).toEqual([[mockTopics[0]]]);
+ });
+});
diff --git a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
index c2bf90e7635..0d6bc1b74fb 100644
--- a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
@@ -25,7 +25,7 @@ describe('AlertManagementEmptyState', () => {
}
});
- const EmptyState = () => wrapper.find(GlEmptyState);
+ const EmptyState = () => wrapper.findComponent(GlEmptyState);
describe('Empty state', () => {
it('shows empty state', () => {
diff --git a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
index bba5fcbbf08..3a5fb99fdf1 100644
--- a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
@@ -28,8 +28,8 @@ describe('AlertManagementList', () => {
describe('Alert List Wrapper', () => {
it('should show the empty state when alerts are not enabled', () => {
- expect(wrapper.find(AlertManagementEmptyState).exists()).toBe(true);
- expect(wrapper.find(AlertManagementTable).exists()).toBe(false);
+ expect(wrapper.findComponent(AlertManagementEmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(AlertManagementTable).exists()).toBe(false);
});
it('should show the alerts table when alerts are enabled', () => {
@@ -39,8 +39,8 @@ describe('AlertManagementList', () => {
},
});
- expect(wrapper.find(AlertManagementEmptyState).exists()).toBe(false);
- expect(wrapper.find(AlertManagementTable).exists()).toBe(true);
+ expect(wrapper.findComponent(AlertManagementEmptyState).exists()).toBe(false);
+ expect(wrapper.findComponent(AlertManagementTable).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 5b823694b99..3e1438c37d6 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -172,8 +172,8 @@ describe('AlertManagementTable', () => {
await nextTick();
- expect(wrapper.find(GlTable).exists()).toBe(true);
- expect(findAlertsTable().find(GlIcon).classes('icon-critical')).toBe(true);
+ expect(wrapper.findComponent(GlTable).exists()).toBe(true);
+ expect(findAlertsTable().findComponent(GlIcon).classes('icon-critical')).toBe(true);
});
it('renders severity text', () => {
@@ -200,7 +200,7 @@ describe('AlertManagementTable', () => {
loading: false,
});
- const avatar = findAssignees().at(1).find(GlAvatar);
+ const avatar = findAssignees().at(1).findComponent(GlAvatar);
const { src, label } = avatar.attributes();
const { name, avatarUrl } = mockAlerts[1].assignees.nodes[0];
diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
index dba9c8be669..1e125bdfd3a 100644
--- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -47,7 +47,7 @@ describe('AlertMappingBuilder', () => {
expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle);
expect(findColumnInRow(0, 3).text()).toContain(i18n.columns.fallbackKeyTitle);
- const fallbackColumnIcon = findColumnInRow(0, 3).find(GlIcon);
+ const fallbackColumnIcon = findColumnInRow(0, 3).findComponent(GlIcon);
expect(fallbackColumnIcon.exists()).toBe(true);
expect(fallbackColumnIcon.attributes('name')).toBe('question');
expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip);
@@ -55,7 +55,7 @@ describe('AlertMappingBuilder', () => {
it('renders disabled form input for each mapped field', () => {
alertFields.forEach((field, index) => {
- const input = findColumnInRow(index + 1, 0).find(GlFormInput);
+ const input = findColumnInRow(index + 1, 0).findComponent(GlFormInput);
const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
expect(input.attributes('value')).toBe(`${field.label} (${types})`);
expect(input.attributes('disabled')).toBe('');
@@ -71,7 +71,7 @@ describe('AlertMappingBuilder', () => {
it('renders mapping dropdown for each field', () => {
alertFields.forEach(({ types }, index) => {
- const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
+ const dropdown = findColumnInRow(index + 1, 2).findComponent(GlDropdown);
const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types);
expect(dropdown.exists()).toBe(true);
@@ -82,7 +82,7 @@ describe('AlertMappingBuilder', () => {
it('renders fallback dropdown only for the fields that have fallback', () => {
alertFields.forEach(({ types, numberOfFallbacks }, index) => {
- const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown);
+ const dropdown = findColumnInRow(index + 1, 3).findComponent(GlDropdown);
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
if (numberOfFallbacks) {
@@ -96,8 +96,8 @@ describe('AlertMappingBuilder', () => {
it('emits event with selected mapping', () => {
const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' };
jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave);
- const dropdown = findColumnInRow(1, 2).find(GlDropdown);
- const option = dropdown.find(GlDropdownItem);
+ const dropdown = findColumnInRow(1, 2).findComponent(GlDropdown);
+ const option = dropdown.findComponent(GlDropdownItem);
option.vm.$emit('click');
expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]);
});
diff --git a/spec/frontend/alerts_settings/components/alerts_form_spec.js b/spec/frontend/alerts_settings/components/alerts_form_spec.js
index a045954dfb8..33098282bf8 100644
--- a/spec/frontend/alerts_settings/components/alerts_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_form_spec.js
@@ -5,7 +5,7 @@ describe('Alert integration settings form', () => {
let wrapper;
const service = { updateSettings: jest.fn().mockResolvedValue() };
- const findForm = () => wrapper.find({ ref: 'settingsForm' });
+ const findForm = () => wrapper.findComponent({ ref: 'settingsForm' });
beforeEach(() => {
wrapper = shallowMount(AlertsSettingsForm, {
diff --git a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index 3ffbb7ab60a..9983af873c2 100644
--- a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
@@ -53,8 +53,8 @@ describe('AlertIntegrationsList', () => {
mountComponent();
});
- const findTableComponent = () => wrapper.find(GlTable);
- const findTableComponentRows = () => wrapper.find(GlTable).findAll('table tbody tr');
+ const findTableComponent = () => wrapper.findComponent(GlTable);
+ const findTableComponentRows = () => wrapper.findComponent(GlTable).findAll('table tbody tr');
const finsStatusCell = () => wrapper.findAll('[data-testid="integration-activated-status"]');
it('renders a table', () => {
@@ -67,7 +67,7 @@ describe('AlertIntegrationsList', () => {
});
it('renders an an edit and delete button for each integration', () => {
- expect(findTableComponent().findAll(GlButton).length).toBe(4);
+ expect(findTableComponent().findAllComponents(GlButton).length).toBe(4);
});
it('renders an highlighted row when a current integration is selected to edit', () => {
@@ -78,7 +78,7 @@ describe('AlertIntegrationsList', () => {
describe('integration status', () => {
it('enabled', () => {
const cell = finsStatusCell().at(0);
- const activatedIcon = cell.find(GlIcon);
+ const activatedIcon = cell.findComponent(GlIcon);
expect(cell.text()).toBe(i18n.status.enabled.name);
expect(activatedIcon.attributes('name')).toBe('check');
expect(activatedIcon.attributes('title')).toBe(i18n.status.enabled.tooltip);
@@ -86,7 +86,7 @@ describe('AlertIntegrationsList', () => {
it('disabled', () => {
const cell = finsStatusCell().at(1);
- const notActivatedIcon = cell.find(GlIcon);
+ const notActivatedIcon = cell.findComponent(GlIcon);
expect(cell.text()).toBe(i18n.status.disabled.name);
expect(notActivatedIcon.attributes('name')).toBe('warning-solid');
expect(notActivatedIcon.attributes('title')).toBe(i18n.status.disabled.tooltip);
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 7d9d2875cf8..fb9e97e7505 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -325,9 +325,9 @@ describe('AlertsSettingsForm', () => {
});
await nextTick();
- expect(findSamplePayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(
- disabled,
- );
+ expect(
+ findSamplePayloadSection().findComponent(GlFormTextarea).attributes('disabled'),
+ ).toBe(disabled);
});
});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index ed185c11732..0266adeb6c7 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -63,14 +63,14 @@ describe('AlertsSettingsWrapper', () => {
const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon);
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
- const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
+ const findIntegrations = () => wrapper.findComponent(IntegrationsList).findAll('table tbody tr');
const findAddIntegrationBtn = () => wrapper.findByTestId('add-integration-btn');
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
const findAlert = () => wrapper.findComponent(GlAlert);
function destroyHttpIntegration(localWrapper) {
localWrapper
- .find(IntegrationsList)
+ .findComponent(IntegrationsList)
.vm.$emit('delete-integration', { id: integrationToDestroy.id });
}
@@ -148,7 +148,7 @@ describe('AlertsSettingsWrapper', () => {
expect(findIntegrations()).toHaveLength(mockIntegrations.length);
});
- it('renders `Add new integration` button when multiple integrations are supported ', () => {
+ it('renders `Add new integration` button when multiple integrations are supported', () => {
createComponent({
data: {
integrations: mockIntegrations,
@@ -189,7 +189,7 @@ describe('AlertsSettingsWrapper', () => {
data: { integrations: [] },
loading: true,
});
- expect(wrapper.find(IntegrationsList).exists()).toBe(true);
+ expect(wrapper.findComponent(IntegrationsList).exists()).toBe(true);
expect(findLoader().exists()).toBe(true);
});
});
@@ -321,7 +321,7 @@ describe('AlertsSettingsWrapper', () => {
});
});
- it('shows an error alert when integration creation fails ', async () => {
+ it('shows an error alert when integration creation fails', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR);
findAlertsSettingsForm().vm.$emit('create-new-integration', {});
@@ -330,7 +330,7 @@ describe('AlertsSettingsWrapper', () => {
expect(createFlash).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR });
});
- it('shows an error alert when integration token reset fails ', async () => {
+ it('shows an error alert when integration token reset fails', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR);
findAlertsSettingsForm().vm.$emit('reset-token', {});
@@ -339,7 +339,7 @@ describe('AlertsSettingsWrapper', () => {
expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
});
- it('shows an error alert when integration update fails ', async () => {
+ it('shows an error alert when integration update fails', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
findAlertsSettingsForm().vm.$emit('update-integration', {});
@@ -357,14 +357,14 @@ describe('AlertsSettingsWrapper', () => {
mock.restore();
});
- it('shows an error alert when integration test payload is invalid ', async () => {
+ it('shows an error alert when integration test payload is invalid', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createFlash).toHaveBeenCalledTimes(1);
});
- it('shows an error alert when integration is not activated ', async () => {
+ it('shows an error alert when integration is not activated', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.FORBIDDEN);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createFlash).toHaveBeenCalledWith({
diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/components/activity_chart_spec.js
index a6b45ffe20f..c26407f5c1d 100644
--- a/spec/frontend/analytics/components/activity_chart_spec.js
+++ b/spec/frontend/analytics/components/activity_chart_spec.js
@@ -18,7 +18,7 @@ describe('Activity Chart Bundle', () => {
wrapper = null;
});
- const findChart = () => wrapper.find(GlColumnChart);
+ const findChart = () => wrapper.findComponent(GlColumnChart);
const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
describe('Activity Chart', () => {
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index a38df274243..7a09fe3319d 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -1,5 +1,5 @@
-import { GlDaterangePicker, GlSprintf } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlDaterangePicker } from '@gitlab/ui';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import Daterange from '~/analytics/shared/components/daterange.vue';
@@ -13,13 +13,12 @@ describe('Daterange component', () => {
let wrapper;
- const factory = (props = defaultProps, mountFn = shallowMount) => {
+ const factory = (props = defaultProps, mountFn = shallowMountExtended) => {
wrapper = mountFn(Daterange, {
propsData: {
...defaultProps,
...props,
},
- stubs: { GlSprintf },
});
};
@@ -28,7 +27,7 @@ describe('Daterange component', () => {
});
const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker);
- const findDateRangeIndicator = () => wrapper.findComponent(GlSprintf);
+ const findDateRangeIndicator = () => wrapper.findByTestId('daterange-picker-indicator');
describe('template', () => {
describe('when show is false', () => {
@@ -52,7 +51,7 @@ describe('Daterange component', () => {
const endDate = new Date('2019-09-30');
const minDate = new Date('2019-06-01');
- factory({ show: true, startDate, endDate, minDate }, mount);
+ factory({ show: true, startDate, endDate, minDate }, mountExtended);
const input = findDaterangePicker().find('input');
input.setValue('2019-01-01');
@@ -64,7 +63,7 @@ describe('Daterange component', () => {
describe('with a maxDateRange being set', () => {
beforeEach(() => {
- factory({ maxDateRange: 30 });
+ factory({ maxDateRange: 30 }, mountExtended);
});
it('displays the max date range indicator', () => {
@@ -72,7 +71,7 @@ describe('Daterange component', () => {
});
it('displays the correct number of selected days in the indicator', () => {
- expect(findDateRangeIndicator().text()).toMatchInterpolatedText('10 days selected');
+ expect(findDateRangeIndicator().text()).toBe('10 days selected');
});
it('sets the tooltip', () => {
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index ffec77c2708..6a58f8c6d29 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -30,7 +30,7 @@ describe('MetricPopover', () => {
const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]');
const findMetricDescription = () => wrapper.findByTestId('metric-description');
const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
- const findMetricDocsLinkIcon = () => findMetricDocsLink().find(GlIcon);
+ const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
@@ -83,7 +83,9 @@ describe('MetricPopover', () => {
const allLinkContainers = findAllMetricLinks();
expect(allLinkContainers.at(idx).text()).toContain(link.name);
- expect(allLinkContainers.at(idx).find(GlLink).attributes('href')).toBe(link.url);
+ expect(allLinkContainers.at(idx).findComponent(GlLink).attributes('href')).toBe(
+ link.url,
+ );
});
});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 69918c1db65..3871fd530d8 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -79,11 +79,11 @@ describe('ProjectsDropdownFilter component', () => {
const findClearAllButton = () => wrapper.findByText('Clear all');
const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate);
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () =>
findDropdown()
- .findAll(GlDropdownItem)
+ .findAllComponents(GlDropdownItem)
.filter((w) => w.text() !== 'No matching results');
const findDropdownAtIndex = (index) => findDropdownItems().at(index);
@@ -106,7 +106,7 @@ describe('ProjectsDropdownFilter component', () => {
};
// NOTE: Selected items are now visually separated from unselected items
- const findSelectedDropdownItems = () => findHighlightedItems().findAll(GlDropdownItem);
+ const findSelectedDropdownItems = () => findHighlightedItems().findAllComponents(GlDropdownItem);
const findSelectedDropdownAtIndex = (index) => findSelectedDropdownItems().at(index);
const findSelectedButtonIdentIconAtIndex = (index) =>
diff --git a/spec/frontend/analytics/usage_trends/components/app_spec.js b/spec/frontend/analytics/usage_trends/components/app_spec.js
index 156be26f895..c732dc22322 100644
--- a/spec/frontend/analytics/usage_trends/components/app_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/app_spec.js
@@ -21,13 +21,13 @@ describe('UsageTrendsApp', () => {
});
it('displays the usage counts component', () => {
- expect(wrapper.find(UsageCounts).exists()).toBe(true);
+ expect(wrapper.findComponent(UsageCounts).exists()).toBe(true);
});
['Total projects & groups', 'Pipelines', 'Issues & merge requests'].forEach((usage) => {
it(`displays the ${usage} chart`, () => {
const chartTitles = wrapper
- .findAll(UsageTrendsCountChart)
+ .findAllComponents(UsageTrendsCountChart)
.wrappers.map((chartComponent) => chartComponent.props('chartTitle'));
expect(chartTitles).toContain(usage);
@@ -35,6 +35,6 @@ describe('UsageTrendsApp', () => {
});
it('displays the users chart component', () => {
- expect(wrapper.find(UsersChart).exists()).toBe(true);
+ expect(wrapper.findComponent(UsersChart).exists()).toBe(true);
});
});
diff --git a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
index 02cf7f42a0b..ad6089f74b5 100644
--- a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
@@ -50,9 +50,9 @@ describe('UsageTrendsCountChart', () => {
wrapper = null;
});
- const findLoader = () => wrapper.find(ChartSkeletonLoader);
- const findChart = () => wrapper.find(GlLineChart);
- const findAlert = () => wrapper.find(GlAlert);
+ const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
+ const findChart = () => wrapper.findComponent(GlLineChart);
+ const findAlert = () => wrapper.findComponent(GlAlert);
describe('while loading', () => {
beforeEach(() => {
@@ -61,7 +61,7 @@ describe('UsageTrendsCountChart', () => {
});
it('requests data', () => {
- expect(queryHandler).toBeCalledTimes(1);
+ expect(queryHandler).toHaveBeenCalledTimes(1);
});
it('displays the skeleton loader', () => {
@@ -105,7 +105,7 @@ describe('UsageTrendsCountChart', () => {
});
it('requests data', () => {
- expect(queryHandler).toBeCalledTimes(1);
+ expect(queryHandler).toHaveBeenCalledTimes(1);
});
it('hides the skeleton loader', () => {
@@ -141,7 +141,7 @@ describe('UsageTrendsCountChart', () => {
});
it('requests data twice', () => {
- expect(queryHandler).toBeCalledTimes(2);
+ expect(queryHandler).toHaveBeenCalledTimes(2);
});
it('passes the data to the line chart', () => {
diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
index 32a664a5026..e7abd4d4323 100644
--- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
@@ -47,9 +47,9 @@ describe('UsersChart', () => {
wrapper = null;
});
- const findLoader = () => wrapper.find(ChartSkeletonLoader);
- const findAlert = () => wrapper.find(GlAlert);
- const findChart = () => wrapper.find(GlAreaChart);
+ const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findChart = () => wrapper.findComponent(GlAreaChart);
describe('while loading', () => {
beforeEach(() => {
@@ -139,7 +139,7 @@ describe('UsersChart', () => {
});
it('requests data twice', () => {
- expect(queryHandler).toBeCalledTimes(2);
+ expect(queryHandler).toHaveBeenCalledTimes(2);
});
it('calls fetchMore', () => {
diff --git a/spec/frontend/analytics/usage_trends/utils_spec.js b/spec/frontend/analytics/usage_trends/utils_spec.js
index 656f310dda7..9982e96735e 100644
--- a/spec/frontend/analytics/usage_trends/utils_spec.js
+++ b/spec/frontend/analytics/usage_trends/utils_spec.js
@@ -16,17 +16,17 @@ describe('getAverageByMonth', () => {
expect(getAverageByMonth(mockCountsData2)).toStrictEqual(countsMonthlyChartData2);
});
- it('it transforms a data point to the first of the month', () => {
+ it('transforms a data point to the first of the month', () => {
const item = mockCountsData1[0];
const firstOfTheMonth = item.recordedAt.replace(/-[0-9]{2}$/, '-01');
expect(getAverageByMonth([item])).toStrictEqual([[firstOfTheMonth, item.count]]);
});
- it('it uses sane defaults', () => {
+ it('uses sane defaults', () => {
expect(getAverageByMonth()).toStrictEqual([]);
});
- it('it errors when passing null', () => {
+ it('errors when passing null', () => {
expect(() => {
getAverageByMonth(null);
}).toThrow();
diff --git a/spec/frontend/api/harbor_registry_spec.js b/spec/frontend/api/harbor_registry_spec.js
new file mode 100644
index 00000000000..8a4c377ebd1
--- /dev/null
+++ b/spec/frontend/api/harbor_registry_spec.js
@@ -0,0 +1,107 @@
+import MockAdapter from 'axios-mock-adapter';
+import * as harborRegistryApi from '~/api/harbor_registry';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+
+describe('~/api/harbor_registry', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ jest.spyOn(axios, 'get');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('getHarborRepositoriesList', () => {
+ it('fetches the harbor repositories of the configured harbor project', () => {
+ const requestPath = '/flightjs/Flight/-/harbor/repositories';
+ const expectedUrl = `${requestPath}.json`;
+ const expectedParams = {
+ limit: 10,
+ page: 1,
+ sort: 'update_time desc',
+ requestPath,
+ };
+ const expectResponse = [
+ {
+ harbor_id: 1,
+ name: 'test-project/image-1',
+ artifact_count: 1,
+ creation_time: '2022-07-16T08:20:34.851Z',
+ update_time: '2022-07-16T08:20:34.851Z',
+ harbor_project_id: 2,
+ pull_count: 0,
+ location: 'http://demo.harbor.com/harbor/projects/2/repositories/image-1',
+ },
+ ];
+ mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
+
+ return harborRegistryApi.getHarborRepositoriesList(expectedParams).then(({ data }) => {
+ expect(data).toEqual(expectResponse);
+ });
+ });
+ });
+
+ describe('getHarborArtifacts', () => {
+ it('fetches the artifacts of a particular harbor repository', () => {
+ const requestPath = '/flightjs/Flight/-/harbor/repositories';
+ const repoName = 'image-1';
+ const expectedUrl = `${requestPath}/${repoName}/artifacts.json`;
+ const expectedParams = {
+ limit: 10,
+ page: 1,
+ sort: 'name asc',
+ repoName,
+ requestPath,
+ };
+ const expectResponse = [
+ {
+ harbor_id: 1,
+ digest: 'sha256:dcdf379c574e1773d703f0c0d56d67594e7a91d6b84d11ff46799f60fb081c52',
+ size: 775241,
+ push_time: '2022-07-16T08:20:34.867Z',
+ tags: ['v2', 'v1', 'latest'],
+ },
+ ];
+ mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
+
+ return harborRegistryApi.getHarborArtifacts(expectedParams).then(({ data }) => {
+ expect(data).toEqual(expectResponse);
+ });
+ });
+ });
+
+ describe('getHarborTags', () => {
+ it('fetches the tags of a particular artifact', () => {
+ const requestPath = '/flightjs/Flight/-/harbor/repositories';
+ const repoName = 'image-1';
+ const digest = 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35';
+ const expectedUrl = `${requestPath}/${repoName}/artifacts/${digest}/tags.json`;
+ const expectedParams = {
+ requestPath,
+ digest,
+ repoName,
+ };
+ const expectResponse = [
+ {
+ repositoryId: 4,
+ artifactId: 5,
+ id: 4,
+ name: 'latest',
+ pullTime: '0001-01-01T00:00:00.000Z',
+ pushTime: '2022-05-27T18:21:27.903Z',
+ signed: false,
+ immutable: false,
+ },
+ ];
+ mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
+
+ return harborRegistryApi.getHarborTags(expectedParams).then(({ data }) => {
+ expect(data).toEqual(expectResponse);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
index 2f3ff2b22f2..ca94acfa444 100644
--- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -39,8 +39,8 @@ describe('Keep latest artifact checkbox', () => {
const fullPath = 'gitlab-org/gitlab';
const helpPagePath = '/help/ci/pipelines/job_artifacts';
- const findCheckbox = () => wrapper.find(GlFormCheckbox);
- const findHelpLink = () => wrapper.find(GlLink);
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findHelpLink = () => wrapper.findComponent(GlLink);
const createComponent = (handlers) => {
requestHandlers = {
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
index 2dcc537809f..0d9196b88ed 100644
--- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -31,11 +31,13 @@ describe('RecoveryCodes', () => {
};
const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findRecoveryCodes = () => wrapper.findByTestId('recovery-codes');
- const findCopyButton = () => wrapper.find(ClipboardButton);
+ const findCopyButton = () => wrapper.findComponent(ClipboardButton);
const findButtonByText = (text) =>
- wrapper.findAll(GlButton).wrappers.find((buttonWrapper) => buttonWrapper.text() === text);
+ wrapper
+ .findAllComponents(GlButton)
+ .wrappers.find((buttonWrapper) => buttonWrapper.text() === text);
const findDownloadButton = () => findButtonByText('Download codes');
const findPrintButton = () => findButtonByText('Print codes');
const findProceedButton = () => findButtonByText('Proceed');
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index f9a6b2df662..427159bacd9 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -10,7 +10,7 @@ describe('initRecoveryCodes', () => {
let el;
let wrapper;
- const findRecoveryCodesComponent = () => wrapper.find(RecoveryCodes);
+ const findRecoveryCodesComponent = () => wrapper.findComponent(RecoveryCodes);
beforeEach(() => {
el = document.createElement('div');
diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js
index c881e0f9794..7a9262cd004 100644
--- a/spec/frontend/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -8,6 +8,7 @@ describe('Autosave', () => {
let autosave;
const field = $('<textarea></textarea>');
+ const checkbox = $('<input type="checkbox">');
const key = 'key';
const fallbackKey = 'fallbackKey';
const lockVersionKey = 'lockVersionKey';
@@ -90,6 +91,24 @@ describe('Autosave', () => {
expect(eventHandler).toHaveBeenCalledTimes(1);
fieldElement.removeEventListener('change', eventHandler);
});
+
+ describe('if field type is checkbox', () => {
+ beforeEach(() => {
+ autosave = {
+ field: checkbox,
+ key,
+ isLocalStorageAvailable: true,
+ type: 'checkbox',
+ };
+ });
+
+ it('should restore', () => {
+ window.localStorage.setItem(key, true);
+ expect(checkbox.is(':checked')).toBe(false);
+ Autosave.prototype.restore.call(autosave);
+ expect(checkbox.is(':checked')).toBe(true);
+ });
+ });
});
describe('if field gets deleted from DOM', () => {
@@ -169,6 +188,31 @@ describe('Autosave', () => {
expect(window.localStorage.setItem).toHaveBeenCalled();
});
});
+
+ describe('if field type is checkbox', () => {
+ beforeEach(() => {
+ autosave = {
+ field: checkbox,
+ key,
+ isLocalStorageAvailable: true,
+ type: 'checkbox',
+ };
+ });
+
+ it('should save true when checkbox on', () => {
+ checkbox.prop('checked', true);
+ Autosave.prototype.save.call(autosave);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(key, true);
+ });
+
+ it('should call reset when checkbox off', () => {
+ autosave.reset = jest.fn();
+ checkbox.prop('checked', false);
+ Autosave.prototype.save.call(autosave);
+ expect(autosave.reset).toHaveBeenCalled();
+ expect(window.localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
});
describe('save with lockVersion', () => {
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index 79cf5f3e4ff..bddb6d3801c 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -42,7 +42,7 @@ describe('BadgeSettings component', () => {
button.vm.$emit('click');
await nextTick();
- const modal = wrapper.find(GlModal);
+ const modal = wrapper.findComponent(GlModal);
expect(modal.isVisible()).toBe(true);
});
@@ -51,7 +51,7 @@ describe('BadgeSettings component', () => {
});
it('displays badge list', () => {
- expect(wrapper.find(BadgeList).isVisible()).toBe(true);
+ expect(wrapper.findComponent(BadgeList).isVisible()).toBe(true);
});
describe('when editing', () => {
@@ -64,7 +64,7 @@ describe('BadgeSettings component', () => {
});
it('displays no badge list', () => {
- expect(wrapper.find(BadgeList).isVisible()).toBe(false);
+ expect(wrapper.findComponent(BadgeList).isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
index 6a5ff1af7c9..c922d6a9809 100644
--- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
+++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
@@ -35,13 +35,13 @@ describe('Batch comments diff file drafts component', () => {
it('renders list of draft notes', () => {
factory();
- expect(vm.findAll(DraftNote).length).toEqual(2);
+ expect(vm.findAllComponents(DraftNote).length).toEqual(2);
});
it('renders index of draft note', () => {
factory();
- const elements = vm.findAll(DesignNotePin);
+ const elements = vm.findAllComponents(DesignNotePin);
expect(elements.length).toEqual(2);
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index ccca4a2c3e9..03ecbc01a56 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -61,7 +61,7 @@ describe('Batch comments draft note component', () => {
createComponent();
expect(wrapper.findComponent(GlBadge).exists()).toBe(true);
- const note = wrapper.find(NoteableNote);
+ const note = wrapper.findComponent(NoteableNote);
expect(note.exists()).toBe(true);
expect(note.props().note).toEqual(draft);
@@ -115,14 +115,14 @@ describe('Batch comments draft note component', () => {
await nextTick();
const publishNowButton = findSubmitReviewButton();
- expect(publishNowButton.attributes().disabled).toBeTruthy();
+ expect(publishNowButton.attributes().disabled).toBe('true');
});
});
describe('update', () => {
it('dispatches updateDraft', async () => {
createComponent();
- const note = wrapper.find(NoteableNote);
+ const note = wrapper.findComponent(NoteableNote);
note.vm.$emit('handleEdit');
@@ -147,7 +147,7 @@ describe('Batch comments draft note component', () => {
createComponent();
jest.spyOn(window, 'confirm').mockImplementation(() => true);
- const note = wrapper.find(NoteableNote);
+ const note = wrapper.findComponent(NoteableNote);
note.vm.$emit('handleDeleteNote', draft);
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 079b64225e4..283632cb560 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -53,7 +53,7 @@ describe('Batch comments preview dropdown', () => {
});
describe('clicking draft', () => {
- it('it toggles active file when viewDiffsFileByFile is true', async () => {
+ it('toggles active file when viewDiffsFileByFile is true', async () => {
factory({
viewDiffsFileByFile: true,
sortedDrafts: [{ id: 1, file_hash: 'hash' }],
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index cb71edd1238..91e6b84a216 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -118,7 +118,7 @@ describe('Batch comments draft preview item component', () => {
);
});
- it('it renders thread resolved text', () => {
+ it('renders thread resolved text', () => {
expect(vm.$el.querySelector('.draft-note-resolution').textContent).toContain(
'Thread will be resolved',
);
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index a3168931f1f..d1b7160d231 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -28,12 +28,12 @@ describe('Batch comments publish dropdown component', () => {
it('renders list of drafts', () => {
createComponent();
- expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
+ expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2);
});
it('renders draft count in dropdown title', () => {
createComponent();
- expect(wrapper.find(GlDropdown).props('headerText')).toEqual('2 pending comments');
+ expect(wrapper.findComponent(GlDropdown).props('headerText')).toEqual('2 pending comments');
});
});
diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js
index f50db6ab210..0a4c9ff62e4 100644
--- a/spec/frontend/batch_comments/components/review_bar_spec.js
+++ b/spec/frontend/batch_comments/components/review_bar_spec.js
@@ -24,7 +24,7 @@ describe('Batch comments review bar component', () => {
wrapper.destroy();
});
- it('it adds review-bar-visible class to body when review bar is mounted', async () => {
+ it('adds review-bar-visible class to body when review bar is mounted', async () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
createComponent();
@@ -32,7 +32,7 @@ describe('Batch comments review bar component', () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
});
- it('it removes review-bar-visible class to body when review bar is destroyed', async () => {
+ it('removes review-bar-visible class to body when review bar is destroyed', async () => {
createComponent();
wrapper.destroy();
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 4f5ff797230..462ef7e7280 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -8,7 +8,7 @@ Vue.use(Vuex);
let wrapper;
let publishReview;
-function factory() {
+function factory({ canApprove = true } = {}) {
publishReview = jest.fn();
const store = new Vuex.Store({
@@ -17,8 +17,13 @@ function factory() {
markdownDocsPath: '/markdown/docs',
quickActionsDocsPath: '/quickactions/docs',
}),
- getNoteableData: () => ({ id: 1, preview_note_path: '/preview' }),
+ getNoteableData: () => ({
+ id: 1,
+ preview_note_path: '/preview',
+ current_user: { can_approve: canApprove },
+ }),
noteableType: () => 'merge_request',
+ getCurrentUserLastNote: () => ({ id: 1 }),
},
modules: {
batchComments: {
@@ -41,6 +46,7 @@ const findForm = () => wrapper.findByTestId('submit-gl-form');
describe('Batch comments submit dropdown', () => {
afterEach(() => {
wrapper.destroy();
+ window.mrTabs = null;
});
it('calls publishReview with note data', async () => {
@@ -54,9 +60,24 @@ describe('Batch comments submit dropdown', () => {
noteable_type: 'merge_request',
noteable_id: 1,
note: 'Hello world',
+ approve: false,
+ approval_password: '',
});
});
+ it('switches to the overview tab after submit', async () => {
+ window.mrTabs = { tabShown: jest.fn() };
+
+ factory();
+
+ findCommentTextarea().setValue('Hello world');
+
+ await findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+ await Vue.nextTick();
+
+ expect(window.mrTabs.tabShown).toHaveBeenCalledWith('show');
+ });
+
it('sets submit dropdown to loading', async () => {
factory();
@@ -66,4 +87,14 @@ describe('Batch comments submit dropdown', () => {
expect(findSubmitButton().props('loading')).toBe(true);
});
+
+ it.each`
+ canApprove | exists | existsText
+ ${true} | ${true} | ${'shows'}
+ ${false} | ${false} | ${'hides'}
+ `('$existsText approve checkbox if can_approve is $canApprove', ({ canApprove, exists }) => {
+ factory({ canApprove });
+
+ expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
+ });
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 9f50b12bac2..6369ea9aa15 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -180,6 +180,7 @@ describe('Batch comments store actions', () => {
});
it('calls service with notes data', () => {
+ mock.onAny().reply(200);
jest.spyOn(axios, 'post');
return actions
@@ -192,7 +193,7 @@ describe('Batch comments store actions', () => {
it('dispatches error commits', () => {
mock.onAny().reply(500);
- return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
+ return actions.publishReview({ dispatch, commit, getters, rootGetters }).catch(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_ERROR']);
});
diff --git a/spec/frontend/behaviors/bind_in_out_spec.js b/spec/frontend/behaviors/bind_in_out_spec.js
index 49425a9377e..4d958e30b4d 100644
--- a/spec/frontend/behaviors/bind_in_out_spec.js
+++ b/spec/frontend/behaviors/bind_in_out_spec.js
@@ -33,7 +33,7 @@ describe('BindInOut', () => {
testContext.bindInOut = new BindInOut({ tagName: 'INPUT' });
});
- it('should set .eventType to keyup ', () => {
+ it('should set .eventType to keyup', () => {
expect(testContext.bindInOut.eventType).toEqual('keyup');
});
});
@@ -43,7 +43,7 @@ describe('BindInOut', () => {
testContext.bindInOut = new BindInOut({ tagName: 'TEXTAREA' });
});
- it('should set .eventType to keyup ', () => {
+ it('should set .eventType to keyup', () => {
expect(testContext.bindInOut.eventType).toEqual('keyup');
});
});
@@ -53,7 +53,7 @@ describe('BindInOut', () => {
testContext.bindInOut = new BindInOut({ tagName: 'SELECT' });
});
- it('should set .eventType to change ', () => {
+ it('should set .eventType to change', () => {
expect(testContext.bindInOut.eventType).toEqual('change');
});
});
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index e8d1f724c4b..4b6cb79791c 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -2,18 +2,6 @@ import SketchLoader from '~/blob/sketch';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
-jest.mock('jszip', () => {
- return {
- loadAsync: jest.fn().mockResolvedValue({
- files: {
- 'previews/preview.png': {
- async: jest.fn().mockResolvedValue('foo'),
- },
- },
- }),
- };
-});
-
describe('Sketch viewer', () => {
beforeEach(() => {
loadHTMLFixture('static/sketch_viewer.html');
@@ -25,7 +13,7 @@ describe('Sketch viewer', () => {
describe('with error message', () => {
beforeEach(() => {
- jest.spyOn(SketchLoader.prototype, 'getZipFile').mockImplementation(
+ jest.spyOn(SketchLoader.prototype, 'getZipContents').mockImplementation(
() =>
new Promise((resolve, reject) => {
reject();
@@ -50,7 +38,13 @@ describe('Sketch viewer', () => {
describe('success', () => {
beforeEach(() => {
- jest.spyOn(SketchLoader.prototype, 'getZipFile').mockResolvedValue();
+ jest.spyOn(SketchLoader.prototype, 'getZipContents').mockResolvedValue({
+ files: {
+ 'previews/preview.png': {
+ async: jest.fn().mockResolvedValue('foo'),
+ },
+ },
+ });
// eslint-disable-next-line no-new
new SketchLoader(document.getElementById('js-sketch-viewer'));
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 985902b4a3b..2c3ec69f9ae 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -7,6 +7,8 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
+import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
@@ -47,6 +49,8 @@ describe('Board card component', () => {
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
+ const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
+ const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const performSearchMock = jest.fn();
@@ -75,10 +79,12 @@ describe('Board card component', () => {
propsData: {
list,
item: issue,
+ index: 0,
...props,
},
stubs: {
GlLoadingIcon: true,
+ BoardCardMoveToPosition: true,
},
directives: {
GlTooltip: createMockDirective(),
@@ -137,6 +143,20 @@ describe('Board card component', () => {
expect(findHiddenIssueIcon().exists()).toBe(false);
});
+ it('renders the move to position icon', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(true);
+ });
+
+ it('does not render the work type icon by default', () => {
+ expect(findWorkItemIcon().exists()).toBe(false);
+ });
+
+ it('renders the work type icon when props is passed', () => {
+ createWrapper({ item: issue, list, showWorkItemTypeIcon: true });
+ expect(findWorkItemIcon().exists()).toBe(true);
+ expect(findWorkItemIcon().props('workItemType')).toBe(issue.type);
+ });
+
it('renders issue ID with #', () => {
expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`);
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 04192489817..65a41c49e7f 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -75,6 +75,7 @@ export default function createComponent({
id: 1,
iid: 1,
confidential: false,
+ referencePath: 'gitlab-org/test-subgroup/gitlab-test#1',
labels: [],
assignees: [],
...listIssueProps,
diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
index 3fb0706fd10..34e4f996ff0 100644
--- a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
+++ b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
-"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
+"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
<use href=\\"#issue-block\\"></use>
</svg>
<div class=\\"gl-popover\\">
diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js
index cf4ba07da16..ffdc0a7cecc 100644
--- a/spec/frontend/boards/components/board_blocked_icon_spec.js
+++ b/spec/frontend/boards/components/board_blocked_icon_spec.js
@@ -10,13 +10,17 @@ import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
import { truncate } from '~/lib/utils/text_utility';
import {
mockIssue,
+ mockEpic,
mockBlockingIssue1,
mockBlockingIssue2,
+ mockBlockingEpic1,
mockBlockingIssuablesResponse1,
mockBlockingIssuablesResponse2,
mockBlockingIssuablesResponse3,
mockBlockedIssue1,
mockBlockedIssue2,
+ mockBlockedEpic1,
+ mockBlockingEpicIssuablesResponse1,
} from '../mock_data';
describe('BoardBlockedIcon', () => {
@@ -51,9 +55,11 @@ describe('BoardBlockedIcon', () => {
const createWrapperWithApollo = ({
item = mockBlockedIssue1,
blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
+ issuableItem = mockIssue,
+ issuableType = issuableTypes.issue,
} = {}) => {
mockApollo = createMockApollo([
- [blockingIssuablesQueries[issuableTypes.issue].query, blockingIssuablesSpy],
+ [blockingIssuablesQueries[issuableType].query, blockingIssuablesSpy],
]);
Vue.use(VueApollo);
@@ -62,27 +68,34 @@ describe('BoardBlockedIcon', () => {
apolloProvider: mockApollo,
propsData: {
item: {
- ...mockIssue,
+ ...issuableItem,
...item,
},
uniqueId: 'uniqueId',
- issuableType: issuableTypes.issue,
+ issuableType,
},
attachTo: document.body,
}),
);
};
- const createWrapper = ({ item = {}, queries = {}, data = {}, loading = false } = {}) => {
+ const createWrapper = ({
+ item = {},
+ queries = {},
+ data = {},
+ loading = false,
+ mockIssuable = mockIssue,
+ issuableType = issuableTypes.issue,
+ } = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardBlockedIcon, {
propsData: {
item: {
- ...mockIssue,
+ ...mockIssuable,
...item,
},
uniqueId: 'uniqueid',
- issuableType: issuableTypes.issue,
+ issuableType,
},
data() {
return {
@@ -105,11 +118,24 @@ describe('BoardBlockedIcon', () => {
);
};
- it('should render blocked icon', () => {
- createWrapper();
+ it.each`
+ mockIssuable | issuableType | expectedIcon
+ ${mockIssue} | ${issuableTypes.issue} | ${'issue-block'}
+ ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
+ `(
+ 'should render blocked icon for $issuableType',
+ ({ mockIssuable, issuableType, expectedIcon }) => {
+ createWrapper({
+ mockIssuable,
+ issuableType,
+ });
- expect(findGlIcon().exists()).toBe(true);
- });
+ expect(findGlIcon().exists()).toBe(true);
+ const icon = findGlIcon();
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe(expectedIcon);
+ },
+ );
it('should display a loading spinner while loading', () => {
createWrapper({ loading: true });
@@ -124,17 +150,29 @@ describe('BoardBlockedIcon', () => {
});
describe('on mouseenter on blocked icon', () => {
- it('should query for blocking issuables and render the result', async () => {
- createWrapperWithApollo();
+ it.each`
+ item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
+ ${mockBlockedIssue1} | ${issuableTypes.issue} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
+ ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
+ `(
+ 'should query for blocking issuables and render the result for $issuableType',
+ async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => {
+ createWrapperWithApollo({
+ item,
+ issuableType,
+ issuableItem,
+ blockingIssuablesSpy,
+ });
- expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title);
+ expect(findGlPopover().text()).not.toContain(mockBlockingIssuable.title);
- await mouseenter();
+ await mouseenter();
- expect(findGlPopover().exists()).toBe(true);
- expect(findIssuableTitle().text()).toContain(mockBlockingIssue1.title);
- expect(wrapper.vm.skip).toBe(true);
- });
+ expect(findGlPopover().exists()).toBe(true);
+ expect(findIssuableTitle().text()).toContain(mockBlockingIssuable.title);
+ expect(wrapper.vm.skip).toBe(true);
+ },
+ );
it('should emit "blocking-issuables-error" event on query error', async () => {
const mockError = new Error('mayday');
diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js
new file mode 100644
index 00000000000..7254b9486ef
--- /dev/null
+++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js
@@ -0,0 +1,133 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+
+import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
+import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+
+Vue.use(Vuex);
+
+const dropdownOptions = [
+ BoardCardMoveToPosition.i18n.moveToStartText,
+ BoardCardMoveToPosition.i18n.moveToEndText,
+];
+
+describe('Board Card Move to position', () => {
+ let wrapper;
+ let trackingSpy;
+ let store;
+ let dispatch;
+ const itemIndex = 1;
+
+ const createStoreOptions = () => {
+ const state = {
+ pageInfoByListId: {
+ 'gid://gitlab/List/1': {},
+ 'gid://gitlab/List/2': { hasNextPage: true },
+ },
+ };
+ const getters = {
+ getBoardItemsByList: () => () => [mockIssue, mockIssue2, mockIssue3, mockIssue4],
+ };
+ const actions = {
+ moveItem: jest.fn(),
+ };
+
+ return {
+ state,
+ getters,
+ actions,
+ };
+ };
+
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(BoardCardMoveToPosition, {
+ store,
+ propsData: {
+ item: mockIssue2,
+ list: mockList,
+ index: 0,
+ ...propsData,
+ },
+ stubs: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ store = new Vuex.Store(createStoreOptions());
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem);
+ const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
+
+ describe('Dropdown', () => {
+ describe('Dropdown button', () => {
+ it('has an icon with vertical ellipsis', () => {
+ expect(findMoveToPositionDropdown().exists()).toBe(true);
+ expect(findMoveToPositionDropdown().props('icon')).toBe('ellipsis_v');
+ });
+
+ it('is opened on the click of vertical ellipsis and has 2 dropdown items when number of list items < 10', () => {
+ findMoveToPositionDropdown().vm.$emit('click');
+ expect(findDropdownItems()).toHaveLength(dropdownOptions.length);
+ });
+ });
+
+ describe('Dropdown options', () => {
+ beforeEach(() => {
+ createComponent({ index: itemIndex });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ dispatch = jest.spyOn(store, 'dispatch').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it.each`
+ dropdownIndex | dropdownLabel | trackLabel | positionInList
+ ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${0}
+ ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${-1}
+ `(
+ 'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel',
+ async ({ dropdownIndex, dropdownLabel, trackLabel, positionInList }) => {
+ await findMoveToPositionDropdown().vm.$emit('click');
+
+ expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel);
+ await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', {
+ stopPropagation: () => {},
+ });
+
+ await nextTick();
+
+ expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', {
+ category: 'boards:list',
+ label: trackLabel,
+ property: 'type_card',
+ });
+ expect(dispatch).toHaveBeenCalledWith('moveItem', {
+ fromListId: mockList.id,
+ itemId: mockIssue2.id,
+ itemIid: mockIssue2.iid,
+ itemPath: mockIssue2.referencePath,
+ positionInList,
+ toListId: mockList.id,
+ allItemsLoadedInList: true,
+ atIndex: itemIndex,
+ });
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index bb1e63a581e..2feaa5dff8c 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -1,5 +1,5 @@
import { GlLabel } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -45,7 +45,10 @@ describe('Board card', () => {
item = mockIssue,
} = {}) => {
wrapper = mountFn(BoardCard, {
- stubs,
+ stubs: {
+ ...stubs,
+ BoardCardInner,
+ },
store,
propsData: {
list: mockLabelList,
@@ -86,7 +89,7 @@ describe('Board card', () => {
describe('when GlLabel is clicked in BoardCardInner', () => {
it('doesnt call toggleBoardItem', () => {
createStore({ initialState: { isShowingLabels: true } });
- mountComponent({ mountFn: mount, stubs: {} });
+ mountComponent();
wrapper.findComponent(GlLabel).trigger('mouseup');
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index 8b0100d069a..f097f42476a 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -90,7 +90,7 @@ describe('Issue boards new issue form', () => {
});
});
- it('it uses the first issue ID as moveAfterId', async () => {
+ it('uses the first issue ID as moveAfterId', async () => {
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await nextTick();
diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js
index 73340c1b96b..45fa10bf03a 100644
--- a/spec/frontend/boards/components/issue_due_date_spec.js
+++ b/spec/frontend/boards/components/issue_due_date_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import dateFormat from 'dateformat';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import dateFormat from '~/lib/dateformat';
const createComponent = (dueDate = new Date(), closed = false) =>
shallowMount(IssueDueDate, {
diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js
index 06cd3910fc0..0c0c7f66933 100644
--- a/spec/frontend/boards/components/item_count_spec.js
+++ b/spec/frontend/boards/components/item_count_spec.js
@@ -50,7 +50,7 @@ describe('IssueCount', () => {
});
it('contains maxIssueCount in the template', () => {
- expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount));
+ expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount));
});
it('does not have text-danger class when issueSize is less than maxIssueCount', () => {
@@ -75,7 +75,7 @@ describe('IssueCount', () => {
});
it('contains maxIssueCount in the template', () => {
- expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount));
+ expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount));
});
it('has text-danger class', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 1ee05d81f37..dc1f3246be0 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -262,9 +262,11 @@ export const rawIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ type: 'ISSUE',
};
export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
+export const mockEpicFullPath = 'gitlab-org/test-subgroup';
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
@@ -287,6 +289,48 @@ export const mockIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ type: 'ISSUE',
+};
+
+export const mockEpic = {
+ id: 'gid://gitlab/Epic/26',
+ iid: '1',
+ group: {
+ id: 'gid://gitlab/Group/33',
+ fullPath: 'twitter',
+ __typename: 'Group',
+ },
+ title: 'Eum animi debitis occaecati ad non odio repellat voluptatem similique.',
+ state: 'opened',
+ reference: '&1',
+ referencePath: `${mockEpicFullPath}&1`,
+ webPath: `/groups/${mockEpicFullPath}/-/epics/1`,
+ webUrl: `${mockEpicFullPath}/-/epics/1`,
+ createdAt: '2022-01-18T05:15:15Z',
+ closedAt: null,
+ __typename: 'Epic',
+ relativePosition: null,
+ confidential: false,
+ subscribed: true,
+ blocked: true,
+ blockedByCount: 1,
+ labels: {
+ nodes: [],
+ __typename: 'LabelConnection',
+ },
+ hasIssues: true,
+ descendantCounts: {
+ closedEpics: 0,
+ closedIssues: 0,
+ openedEpics: 0,
+ openedIssues: 2,
+ __typename: 'EpicDescendantCount',
+ },
+ descendantWeightSum: {
+ closedIssues: 0,
+ openedIssues: 0,
+ __typename: 'EpicDescendantWeights',
+ },
};
export const mockActiveIssue = {
@@ -521,6 +565,15 @@ export const mockBlockingIssue1 = {
__typename: 'Issue',
};
+export const mockBlockingEpic1 = {
+ id: 'gid://gitlab/Epic/29',
+ iid: '4',
+ title: 'Sint nihil exercitationem aspernatur unde molestiae rem accusantium.',
+ reference: 'twitter&4',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/test-subgroup/-/epics/4',
+ __typename: 'Epic',
+};
+
export const mockBlockingIssue2 = {
id: 'gid://gitlab/Issue/524',
iid: '5',
@@ -562,6 +615,23 @@ export const mockBlockingIssuablesResponse1 = {
},
};
+export const mockBlockingEpicIssuablesResponse1 = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/33',
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/26',
+ blockingIssuables: {
+ __typename: 'EpicConnection',
+ nodes: [mockBlockingEpic1],
+ },
+ },
+ },
+ },
+};
+
export const mockBlockingIssuablesResponse2 = {
data: {
issuable: {
@@ -599,6 +669,12 @@ export const mockBlockedIssue2 = {
webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0',
};
+export const mockBlockedEpic1 = {
+ id: '26',
+ blockedByCount: 1,
+ webUrl: 'http://gdk.test:3000/gitlab-org/test-subgroup/-/epics/1',
+};
+
export const mockMoveIssueParams = {
itemId: 1,
fromListId: 'gid://gitlab/List/1',
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index e48b946ff1b..e919300228a 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1056,6 +1056,8 @@ describe('moveIssueCard and undoMoveIssueCard', () => {
originalIndex = 0,
moveBeforeId = undefined,
moveAfterId = undefined,
+ allItemsLoadedInList = true,
+ listPosition = undefined,
} = {}) => {
state = {
boardLists: {
@@ -1065,12 +1067,28 @@ describe('moveIssueCard and undoMoveIssueCard', () => {
boardItems: { [itemId]: originalIssue },
boardItemsByListId: { [fromListId]: [123] },
};
- params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
+ params = {
+ itemId,
+ fromListId,
+ toListId,
+ moveBeforeId,
+ moveAfterId,
+ listPosition,
+ allItemsLoadedInList,
+ };
moveMutations = [
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
+ payload: {
+ itemId,
+ listId: toListId,
+ moveBeforeId,
+ moveAfterId,
+ listPosition,
+ allItemsLoadedInList,
+ atIndex: originalIndex,
+ },
},
];
undoMutations = [
@@ -1366,9 +1384,17 @@ describe('updateIssueOrder', () => {
state,
[
{
+ type: types.MUTATE_ISSUE_IN_PROGRESS,
+ payload: true,
+ },
+ {
type: types.MUTATE_ISSUE_SUCCESS,
payload: { issue: rawIssue },
},
+ {
+ type: types.MUTATE_ISSUE_IN_PROGRESS,
+ payload: false,
+ },
],
[],
);
@@ -1390,6 +1416,14 @@ describe('updateIssueOrder', () => {
state,
[
{
+ type: types.MUTATE_ISSUE_IN_PROGRESS,
+ payload: true,
+ },
+ {
+ type: types.MUTATE_ISSUE_IN_PROGRESS,
+ payload: false,
+ },
+ {
type: types.SET_ERROR,
payload: 'An error occurred while moving the issue. Please try again.',
},
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 1606ca09d8f..87a183c0441 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -513,6 +513,31 @@ describe('Board Store Mutations', () => {
listState: [mockIssue2.id, mockIssue.id],
},
],
+ [
+ 'to the top of the list',
+ {
+ payload: {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ positionInList: 0,
+ atIndex: 1,
+ },
+ listState: [mockIssue2.id, mockIssue.id],
+ },
+ ],
+ [
+ 'to the bottom of the list when the list is fully loaded',
+ {
+ payload: {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ positionInList: -1,
+ atIndex: 0,
+ allItemsLoadedInList: true,
+ },
+ listState: [mockIssue.id, mockIssue2.id],
+ },
+ ],
])(`inserts an item into a list %s`, (_, { payload, listState }) => {
mutations.ADD_BOARD_ITEM_TO_LIST(state, payload);
diff --git a/spec/frontend/branches/components/divergence_graph_spec.js b/spec/frontend/branches/components/divergence_graph_spec.js
index 3b565539f87..9429a6e982c 100644
--- a/spec/frontend/branches/components/divergence_graph_spec.js
+++ b/spec/frontend/branches/components/divergence_graph_spec.js
@@ -21,7 +21,7 @@ describe('Branch divergence graph component', () => {
maxCommits: 100,
});
- expect(vm.findAll(GraphBar).length).toBe(2);
+ expect(vm.findAllComponents(GraphBar).length).toBe(2);
expect(vm.element).toMatchSnapshot();
});
@@ -45,7 +45,7 @@ describe('Branch divergence graph component', () => {
maxCommits: 100,
});
- expect(vm.findAll(GraphBar).length).toBe(1);
+ expect(vm.findAllComponents(GraphBar).length).toBe(1);
expect(vm.element).toMatchSnapshot();
});
diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js
index b8448f9ff0a..20e69b5a834 100644
--- a/spec/frontend/captcha/captcha_modal_spec.js
+++ b/spec/frontend/captcha/captcha_modal_spec.js
@@ -40,7 +40,7 @@ describe('Captcha Modal', () => {
});
const findGlModal = () => {
- const glModal = wrapper.find(GlModal);
+ const glModal = wrapper.findComponent(GlModal);
jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown'));
jest
diff --git a/spec/frontend/cascading_settings/components/lock_popovers_spec.js b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
index 182e3c1c8ff..9d3275a1ff2 100644
--- a/spec/frontend/cascading_settings/components/lock_popovers_spec.js
+++ b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
@@ -39,7 +39,7 @@ describe('LockPopovers', () => {
wrapper = mountExtended(LockPopovers);
};
- const findPopover = () => extendedWrapper(wrapper.find(GlPopover));
+ const findPopover = () => extendedWrapper(wrapper.findComponent(GlPopover));
const findByTextInPopover = (text, options) =>
findPopover().findByText((_, element) => element.textContent === text, options);
@@ -143,7 +143,7 @@ describe('LockPopovers', () => {
});
it('mounts multiple popovers', () => {
- const popovers = wrapper.findAll(GlPopover).wrappers;
+ const popovers = wrapper.findAllComponents(GlPopover).wrappers;
expectCorrectPopoverTarget(popoverMountEl1, popovers[0]);
expectCorrectPopoverTarget(popoverMountEl2, popovers[1]);
diff --git a/spec/frontend/chronic_duration_spec.js b/spec/frontend/chronic_duration_spec.js
index 32652e13dfc..b063110782a 100644
--- a/spec/frontend/chronic_duration_spec.js
+++ b/spec/frontend/chronic_duration_spec.js
@@ -86,7 +86,7 @@ describe('parseChronicDuration', () => {
describe('when .raiseExceptions set to true', () => {
it('raises with DurationParseError', () => {
- expect(() => parseChronicDuration('23 gobblygoos', { raiseExceptions: true })).toThrowError(
+ expect(() => parseChronicDuration('23 gobblygoos', { raiseExceptions: true })).toThrow(
DurationParseError,
);
});
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js
index 0ad6ed56b0e..ea69a80274e 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci_lint/components/ci_lint_spec.js
@@ -36,9 +36,9 @@ describe('CI Lint', () => {
});
};
- const findEditor = () => wrapper.find(SourceEditor);
- const findAlert = () => wrapper.find(GlAlert);
- const findCiLintResults = () => wrapper.find(CiLintResults);
+ const findEditor = () => wrapper.findComponent(SourceEditor);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findCiLintResults = () => wrapper.findComponent(CiLintResults);
const findValidateBtn = () => wrapper.find('[data-testid="ci-lint-validate"]');
const findClearBtn = () => wrapper.find('[data-testid="ci-lint-clear"]');
diff --git a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
index 04d38a3281a..5273aafbb04 100644
--- a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
+++ b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
@@ -83,7 +83,9 @@ describe('SecureFilesList', () => {
const [secureFile] = secureFiles;
expect(findCell(0, 0).text()).toBe(secureFile.name);
- expect(findCell(0, 1).find(TimeAgoTooltip).props('time')).toBe(secureFile.created_at);
+ expect(findCell(0, 1).findComponent(TimeAgoTooltip).props('time')).toBe(
+ secureFile.created_at,
+ );
});
describe('event tracking', () => {
diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
index 6bf28a67300..01eb08f4ece 100644
--- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
+++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
@@ -16,13 +16,13 @@ describe('TriggersList', () => {
});
};
- const findTable = () => wrapper.find(GlTable);
+ const findTable = () => wrapper.findComponent(GlTable);
const findHeaderAt = (i) => wrapper.findAll('thead th').at(i);
const findRows = () => wrapper.findAll('tbody tr');
const findRowAt = (i) => findRows().at(i);
const findCell = (i, col) => findRowAt(i).findAll('td').at(col);
- const findClipboardBtn = (i) => findCell(i, 0).find(ClipboardButton);
- const findInvalidBadge = (i) => findCell(i, 0).find(GlBadge);
+ const findClipboardBtn = (i) => findCell(i, 0).findComponent(ClipboardButton);
+ const findInvalidBadge = (i) => findCell(i, 0).findComponent(GlBadge);
const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]');
const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]');
@@ -65,17 +65,19 @@ describe('TriggersList', () => {
it('displays a time ago label when last used', () => {
expect(findCell(0, 3).text()).toBe('Never');
- expect(findCell(1, 3).find(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed);
+ expect(findCell(1, 3).findComponent(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed);
});
it('displays actions in a rows', () => {
const [data] = triggers;
+ const confirmWarning =
+ 'By revoking a trigger you will break any processes making use of it. Are you sure?';
expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath);
expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath);
expect(findRevokeBtn(0).attributes('data-method')).toBe('delete');
- expect(findRevokeBtn(0).attributes('data-confirm')).toBeTruthy();
+ expect(findRevokeBtn(0).attributes('data-confirm')).toBe(confirmWarning);
});
describe('when there are no triggers set', () => {
diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
new file mode 100644
index 00000000000..867f8e0cf8f
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
@@ -0,0 +1,215 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/resolvers';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+
+import ciProjectVariables from '~/ci_variable_list/components/ci_project_variables.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+
+import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
+
+import {
+ environmentFetchErrorText,
+ genericMutationErrorText,
+ variableFetchErrorText,
+} from '~/ci_variable_list/constants';
+
+import {
+ devName,
+ mockProjectEnvironments,
+ mockProjectVariables,
+ newVariable,
+ prodName,
+} from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+ projectFullPath: '/namespace/project',
+ projectId: 1,
+};
+
+describe('Ci Project Variable list', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockEnvironments;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ const createComponentWithApollo = async ({ isLoading = false } = {}) => {
+ const handlers = [
+ [getProjectEnvironments, mockEnvironments],
+ [getProjectVariables, mockVariables],
+ ];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciProjectVariables, {
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ };
+
+ beforeEach(() => {
+ mockEnvironments = jest.fn();
+ mockVariables = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockProjectVariables.data.project.ciVariables.nodes,
+ );
+ });
+
+ it('createFlash was not called', () => {
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createFlash with the expected error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+
+ describe('with an error for environments', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockRejectedValue();
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createFlash with the expected error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${addProjectVariable} | ${'add-variable'}
+ ${'update'} | ${updateProjectVariable} | ${'update-variable'}
+ ${'delete'} | ${deleteProjectVariable} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ fullPath: mockProvide.projectFullPath,
+ projectId: convertToGraphQLId('Project', mockProvide.projectId),
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event | mutationName
+ ${'add'} | ${'add-variable'} | ${'addProjectVariable'}
+ ${'update'} | ${'update-variable'} | ${'updateProjectVariable'}
+ ${'delete'} | ${'delete-variable'} | ${'deleteProjectVariable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event, mutationName }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index e5019e3261e..1ea4e4f833b 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -11,6 +11,7 @@ import {
EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
instanceString,
+ variableOptions,
} from '~/ci_variable_list/constants';
import { mockVariablesWithScopes } from '../mocks';
import ModalStub from '../stubs';
@@ -57,21 +58,23 @@ describe('Ci variable modal', () => {
});
};
- const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
+ const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference');
- const findModal = () => wrapper.find(ModalStub);
+ const findModal = () => wrapper.findComponent(ModalStub);
const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip');
const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn');
const deleteVariableButton = () =>
findModal()
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.wrappers.find((button) => button.props('variant') === 'danger');
const findProtectedVariableCheckbox = () =>
wrapper.findByTestId('ci-variable-protected-checkbox');
const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
const findValueField = () => wrapper.find('#ci-variable-value');
const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link');
- const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput);
+ const findEnvScopeInput = () =>
+ wrapper.findByTestId('environment-scope').findComponent(GlFormInput);
+ const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
afterEach(() => {
wrapper.destroy();
@@ -83,7 +86,7 @@ describe('Ci variable modal', () => {
createComponent();
});
- it('shows the submit button as disabled ', () => {
+ it('shows the submit button as disabled', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
});
});
@@ -93,7 +96,7 @@ describe('Ci variable modal', () => {
createComponent({ props: { selectedVariable: mockVariables[0] } });
});
- it('shows the submit button as enabled ', () => {
+ it('shows the submit button as enabled', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
});
@@ -284,6 +287,21 @@ describe('Ci variable modal', () => {
});
});
+ describe('variable type dropdown', () => {
+ describe('default behaviour', () => {
+ beforeEach(() => {
+ createComponent({ mountFn: mountExtended });
+ });
+
+ it('adds each option as a dropdown item', () => {
+ expect(findVariableTypeDropdown().findAll('option')).toHaveLength(variableOptions.length);
+ variableOptions.forEach((v) => {
+ expect(findVariableTypeDropdown().text()).toContain(v.text);
+ });
+ });
+ });
+ });
+
describe('Validations', () => {
const maskError = 'This variable can not be masked.';
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
index b43153d3d7c..4d0c378d10e 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
@@ -18,7 +18,7 @@ describe('Ci Variable Popover', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
index 6681ab91a4a..b607232907b 100644
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
@@ -40,12 +40,12 @@ describe('Ci variable modal', () => {
});
};
- const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
- const findModal = () => wrapper.find(ModalStub);
+ const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
+ const findModal = () => wrapper.findComponent(ModalStub);
const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]');
const deleteVariableButton = () =>
findModal()
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.wrappers.find((button) => button.props('variant') === 'danger');
afterEach(() => {
@@ -213,7 +213,7 @@ describe('Ci variable modal', () => {
const environmentScopeInput = wrapper
.find('[data-testid="environment-scope"]')
- .find(GlFormInput);
+ .findComponent(GlFormInput);
expect(findCiEnvironmentsDropdown().exists()).toBe(false);
expect(environmentScopeInput.attributes('readonly')).toBe('readonly');
});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 89ba77858dc..6d633c8b740 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -1,4 +1,9 @@
-import { variableTypes, groupString, instanceString } from '~/ci_variable_list/constants';
+import {
+ variableTypes,
+ groupString,
+ instanceString,
+ projectString,
+} from '~/ci_variable_list/constants';
export const devName = 'dev';
export const prodName = 'prod';
@@ -11,8 +16,8 @@ export const mockVariables = (kind) => {
key: 'my-var',
masked: false,
protected: true,
- value: 'env_val',
- variableType: variableTypes.variableType,
+ value: 'variable_value',
+ variableType: variableTypes.envType,
},
{
__typename: `Ci${kind}Variable`,
@@ -20,7 +25,7 @@ export const mockVariables = (kind) => {
key: 'secret',
masked: true,
protected: false,
- value: 'the_secret_value',
+ value: 'another_value',
variableType: variableTypes.fileType,
},
];
@@ -77,7 +82,7 @@ export const mockProjectVariables = {
project: {
__typename: 'Project',
id: 1,
- ciVariables: createDefaultVars(),
+ ciVariables: createDefaultVars({ kind: projectString }),
},
},
};
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
index ae750ff426d..c7d07ead09b 100644
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ b/spec/frontend/ci_variable_list/store/mutations_spec.js
@@ -36,7 +36,7 @@ describe('CI variable list mutations', () => {
});
describe('CLEAR_MODAL', () => {
- it('should clear modal state ', () => {
+ it('should clear modal state', () => {
const modalState = {
variable_type: 'Variable',
key: '',
diff --git a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
new file mode 100644
index 00000000000..2af64191a88
--- /dev/null
+++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
@@ -0,0 +1,96 @@
+import { GlLink, GlIcon, GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AgentIntegrationStatusRow from '~/clusters/agents/components/agent_integration_status_row.vue';
+
+const defaultProps = {
+ text: 'Default integration status',
+};
+
+describe('IntegrationStatus', () => {
+ let wrapper;
+
+ const createWrapper = ({ props = {}, glFeatures = {} } = {}) => {
+ wrapper = shallowMount(AgentIntegrationStatusRow, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ glFeatures,
+ },
+ });
+ };
+
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('icon', () => {
+ const icon = 'status-success';
+ const iconClass = 'text-success-500';
+ it.each`
+ props | iconName | iconClassName
+ ${{ icon, iconClass }} | ${icon} | ${iconClass}
+ ${{ icon }} | ${icon} | ${'text-info'}
+ ${{ iconClass }} | ${'information'} | ${iconClass}
+ ${null} | ${'information'} | ${'text-info'}
+ `('displays correct icon when props are $props', ({ props, iconName, iconClassName }) => {
+ createWrapper({ props });
+
+ expect(findIcon().props('name')).toBe(iconName);
+ expect(findIcon().attributes('class')).toContain(iconClassName);
+ });
+ });
+
+ describe('helpUrl', () => {
+ it('displays a link with the correct help url when provided in props', () => {
+ const props = {
+ helpUrl: 'help-page-path',
+ };
+ createWrapper({ props });
+
+ expect(findLink().attributes('href')).toBe(props.helpUrl);
+ expect(findLink().text()).toBe(defaultProps.text);
+ });
+
+ it("displays the text without a link when it's not provided", () => {
+ createWrapper();
+
+ expect(findLink().exists()).toBe(false);
+ expect(wrapper.text()).toBe(defaultProps.text);
+ });
+ });
+
+ describe('badge', () => {
+ it('does not display premium feature badge when featureName is not provided', () => {
+ createWrapper();
+
+ expect(findBadge().exists()).toBe(false);
+ });
+
+ it('does not display premium feature badge when featureName is provided and is available for the project', () => {
+ const props = { featureName: 'feature' };
+ const glFeatures = { feature: true };
+ createWrapper({ props, glFeatures });
+
+ expect(findBadge().exists()).toBe(false);
+ });
+
+ it('displays premium feature badge when featureName is provided and is not available for the project', () => {
+ const props = { featureName: 'feature' };
+ const glFeatures = { feature: false };
+ createWrapper({ props, glFeatures });
+
+ expect(findBadge().props()).toMatchObject({
+ icon: 'license',
+ variant: 'tier',
+ size: 'md',
+ });
+ expect(findBadge().text()).toBe(wrapper.vm.$options.i18n.premiumTitle);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/agents/components/integration_status_spec.js b/spec/frontend/clusters/agents/components/integration_status_spec.js
new file mode 100644
index 00000000000..36f0e622452
--- /dev/null
+++ b/spec/frontend/clusters/agents/components/integration_status_spec.js
@@ -0,0 +1,111 @@
+import { GlCollapse, GlButton, GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import IntegrationStatus from '~/clusters/agents/components/integration_status.vue';
+import AgentIntegrationStatusRow from '~/clusters/agents/components/agent_integration_status_row.vue';
+import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import {
+ INTEGRATION_STATUS_VALID_TOKEN,
+ INTEGRATION_STATUS_NO_TOKEN,
+ INTEGRATION_STATUS_RESTRICTED_CI_CD,
+} from '~/clusters/agents/constants';
+
+const connectedTimeNow = new Date();
+const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+
+describe('IntegrationStatus', () => {
+ let wrapper;
+
+ const createWrapper = (tokens = []) => {
+ wrapper = shallowMountExtended(IntegrationStatus, {
+ propsData: { tokens },
+ });
+ };
+
+ const findCollapseButton = () => wrapper.findComponent(GlButton);
+ const findCollapse = () => wrapper.findComponent(GlCollapse);
+ const findStatusIcon = () => wrapper.findComponent(GlIcon);
+ const findAgentStatus = () => wrapper.findByTestId('agent-status');
+ const findAgentIntegrationStatusRows = () => wrapper.findAllComponents(AgentIntegrationStatusRow);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ lastUsedAt | status | iconName
+ ${null} | ${'Never connected'} | ${'status-neutral'}
+ ${connectedTimeNow} | ${'Connected'} | ${'status-success'}
+ ${connectedTimeInactive} | ${'Not connected'} | ${'status-alert'}
+ `(
+ 'displays correct text and icon when agent connection status is "$status"',
+ ({ lastUsedAt, status, iconName }) => {
+ const tokens = [{ lastUsedAt }];
+ createWrapper(tokens);
+
+ expect(findStatusIcon().props('name')).toBe(iconName);
+ expect(findAgentStatus().text()).toBe(status);
+ },
+ );
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows the collapse toggle button', () => {
+ expect(findCollapseButton().text()).toBe(wrapper.vm.$options.i18n.title);
+ expect(findCollapseButton().attributes()).toMatchObject({
+ variant: 'link',
+ icon: 'chevron-right',
+ size: 'small',
+ });
+ });
+
+ it('sets collapse component as invisible by default', () => {
+ expect(findCollapse().props('visible')).toBeUndefined();
+ });
+ });
+
+ describe('when user clicks collapse toggle', () => {
+ beforeEach(() => {
+ createWrapper();
+ findCollapseButton().vm.$emit('click');
+ });
+
+ it('changes the collapse button icon', () => {
+ expect(findCollapseButton().props('icon')).toBe('chevron-down');
+ });
+
+ it('sets collapse component as visible', () => {
+ expect(findCollapse().attributes('visible')).toBe('true');
+ });
+ });
+
+ describe('integration status details', () => {
+ it.each`
+ agentStatus | tokens | integrationStatuses
+ ${'active'} | ${[{ lastUsedAt: connectedTimeNow }]} | ${[INTEGRATION_STATUS_VALID_TOKEN, INTEGRATION_STATUS_RESTRICTED_CI_CD]}
+ ${'inactive'} | ${[{ lastUsedAt: connectedTimeInactive }]} | ${[INTEGRATION_STATUS_RESTRICTED_CI_CD]}
+ ${'inactive'} | ${[]} | ${[INTEGRATION_STATUS_NO_TOKEN, INTEGRATION_STATUS_RESTRICTED_CI_CD]}
+ ${'unused'} | ${[{ lastUsedAt: null }]} | ${[INTEGRATION_STATUS_RESTRICTED_CI_CD]}
+ ${'unused'} | ${[]} | ${[INTEGRATION_STATUS_NO_TOKEN, INTEGRATION_STATUS_RESTRICTED_CI_CD]}
+ `(
+ 'displays AgentIntegrationStatusRow component with correct properties when agent status is $agentStatus and agent has $tokens.length tokens',
+ ({ tokens, integrationStatuses }) => {
+ createWrapper(tokens);
+
+ expect(findAgentIntegrationStatusRows().length).toBe(integrationStatuses.length);
+
+ integrationStatuses.forEach((integrationStatus, index) => {
+ expect(findAgentIntegrationStatusRows().at(index).props()).toMatchObject({
+ icon: integrationStatus.icon,
+ iconClass: integrationStatus.iconClass,
+ text: integrationStatus.text,
+ helpUrl: integrationStatus.helpUrl || null,
+ featureName: integrationStatus.featureName || null,
+ });
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index f2f073544e3..efa85136b17 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ClusterAgentShow from '~/clusters/agents/components/show.vue';
import TokenTable from '~/clusters/agents/components/token_table.vue';
import ActivityEvents from '~/clusters/agents/components/activity_events_list.vue';
+import IntegrationStatus from '~/clusters/agents/components/integration_status.vue';
import getAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -76,6 +77,7 @@ describe('ClusterAgentShow', () => {
const findTokenCount = () => wrapper.findByTestId('cluster-agent-token-count').text();
const findEESecurityTabSlot = () => wrapper.findByTestId('ee-security-tab');
const findActivity = () => wrapper.findComponent(ActivityEvents);
+ const findIntegrationStatus = () => wrapper.findComponent(IntegrationStatus);
afterEach(() => {
wrapper.destroy();
@@ -107,6 +109,10 @@ describe('ClusterAgentShow', () => {
expect(findCreatedText()).toMatchInterpolatedText('Created by user-1 2 days ago');
});
+ it('displays agent integration status section', () => {
+ expect(findIntegrationStatus().exists()).toBe(true);
+ });
+
it('displays token count', () => {
expect(findTokenCount()).toMatchInterpolatedText(
`${ClusterAgentShow.i18n.tokens} ${defaultClusterAgent.tokens.count}`,
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index b78f0a3686c..9cbb83eedd2 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -36,7 +36,7 @@ describe('AgentTable', () => {
const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').at(at);
const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at);
- const findStatusIcon = (at) => findStatusText(at).find(GlIcon);
+ const findStatusIcon = (at) => findStatusText(at).findComponent(GlIcon);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
const findConfiguration = (at) =>
@@ -113,7 +113,7 @@ describe('AgentTable', () => {
texts,
lineNumber,
}) => {
- const findIcon = () => findVersionText(lineNumber).find(GlIcon);
+ const findIcon = () => findVersionText(lineNumber).findComponent(GlIcon);
const findPopover = () => wrapper.findByTestId(`popover-${agent}`);
const versionWarning = versionMismatch || versionOutdated;
@@ -151,7 +151,7 @@ describe('AgentTable', () => {
`(
'displays config file path as "$agentPath" at line $lineNumber',
({ agentConfig, link, lineNumber }) => {
- const findLink = findConfiguration(lineNumber).find(GlLink);
+ const findLink = findConfiguration(lineNumber).findComponent(GlLink);
expect(findLink.attributes('href')).toBe(link);
expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index 92cfff7d490..bff1e573dbd 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -334,7 +334,7 @@ describe('Agents', () => {
});
it('displays a loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index a9f11e6fdf8..758f6586e1a 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -46,7 +46,7 @@ describe('ClustersAncestorNotice', () => {
});
it('displays link', () => {
- expect(wrapper.find(GlLink).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_main_view_spec.js b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
index 218463b9adf..6f23ed47d2a 100644
--- a/spec/frontend/clusters_list/components/clusters_main_view_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
@@ -142,7 +142,7 @@ describe('ClustersMainViewComponent', () => {
createWrapper({ certificateBasedClustersEnabled: false });
});
- it('it displays only the Agent tab', () => {
+ it('displays only the Agent tab', () => {
expect(findAllTabs()).toHaveLength(1);
const agentTab = findGlTabAtIndex(0);
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 5c7635c1617..a3f42c1f161 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -142,7 +142,7 @@ describe('Clusters', () => {
({ lineNumber, result }) => {
const statuses = findStatuses();
const status = statuses.at(lineNumber);
- expect(status.find(GlLoadingIcon).exists()).toBe(result);
+ expect(status.findComponent(GlLoadingIcon).exists()).toBe(result);
},
);
});
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 964dd005a27..10264d6a011 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -65,7 +65,7 @@ describe('InstallAgentModal', () => {
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default');
diff --git a/spec/frontend/clusters_list/components/node_error_help_text_spec.js b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
index 8187ab75c58..3211ba44eff 100644
--- a/spec/frontend/clusters_list/components/node_error_help_text_spec.js
+++ b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
@@ -11,7 +11,7 @@ describe('NodeErrorHelpText', () => {
await nextTick();
};
- const findPopover = () => wrapper.find(GlPopover);
+ const findPopover = () => wrapper.findComponent(GlPopover);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index b85047dc816..b9be262efd0 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -63,7 +63,7 @@ describe('Code navigation app component', () => {
it('hides popover when no definition set', () => {
factory();
- expect(wrapper.find(Popover).exists()).toBe(false);
+ expect(wrapper.findComponent(Popover).exists()).toBe(false);
});
it('renders popover when definition set', () => {
@@ -73,7 +73,7 @@ describe('Code navigation app component', () => {
currentBlobPath: 'index.js',
});
- expect(wrapper.find(Popover).exists()).toBe(true);
+ expect(wrapper.findComponent(Popover).exists()).toBe(true);
});
it('calls showDefinition when clicking blob viewer', () => {
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index c038c04a0f8..874263e046a 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -115,8 +115,8 @@ describe('Code navigation popover component', () => {
definitionPathPrefix: DEFINITION_PATH_PREFIX,
});
- expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
- expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'code-output' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ ref: 'doc-output' }).exists()).toBe(false);
});
});
@@ -128,8 +128,8 @@ describe('Code navigation popover component', () => {
definitionPathPrefix: DEFINITION_PATH_PREFIX,
});
- expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
- expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ ref: 'code-output' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'doc-output' }).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js
index b8448709f0b..700c912029c 100644
--- a/spec/frontend/code_navigation/utils/index_spec.js
+++ b/spec/frontend/code_navigation/utils/index_spec.js
@@ -17,7 +17,7 @@ describe('getCurrentHoverElement', () => {
value
${'test'}
${undefined}
- `('it returns cached current key', ({ value }) => {
+ `('returns cached current key', ({ value }) => {
if (value) {
cachedData.set('current', value);
}
@@ -52,7 +52,7 @@ describe('addInteractionClass', () => {
${1} | ${0} | ${0}
${1} | ${0} | ${0}
`(
- 'it sets code navigation attributes for line $line and character $char',
+ 'sets code navigation attributes for line $line and character $char',
({ line, char, index }) => {
addInteractionClass({ path: 'index.js', d: { start_line: line, start_char: char } });
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index b1c8ba48475..fddc767953a 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -1,14 +1,24 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import getPipelineStagesQuery from '~/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql';
-import { mockPipelineStagesQueryResponse, mockStages } from './mock_data';
+import * as graphQlUtils from '~/pipelines/components/graph/utils';
+import {
+ mockDownstreamQueryResponse,
+ mockPipelineStagesQueryResponse,
+ mockStages,
+ mockUpstreamDownstreamQueryResponse,
+ mockUpstreamQueryResponse,
+} from './mock_data';
jest.mock('~/flash');
@@ -17,61 +27,219 @@ Vue.use(VueApollo);
describe('Commit box pipeline mini graph', () => {
let wrapper;
- const findMiniGraph = () => wrapper.findByTestId('commit-box-mini-graph');
- const findUpstream = () => wrapper.findByTestId('commit-box-mini-graph-upstream');
- const findDownstream = () => wrapper.findByTestId('commit-box-mini-graph-downstream');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+ const downstreamHandler = jest.fn().mockResolvedValue(mockDownstreamQueryResponse);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const stagesHandler = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse);
+ const upstreamDownstreamHandler = jest
+ .fn()
+ .mockResolvedValue(mockUpstreamDownstreamQueryResponse);
+ const upstreamHandler = jest.fn().mockResolvedValue(mockUpstreamQueryResponse);
+ const advanceToNextFetch = () => {
+ jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL);
+ };
- const createComponent = ({ props = {} } = {}) => {
- const handlers = [
- [getLinkedPipelinesQuery, {}],
+ const fullPath = 'gitlab-org/gitlab';
+ const iid = '315';
+ const createMockApolloProvider = (handler = downstreamHandler) => {
+ const requestHandlers = [
+ [getLinkedPipelinesQuery, handler],
[getPipelineStagesQuery, stagesHandler],
];
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (handler) => {
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
stages: mockStages,
- ...props,
},
- apolloProvider: createMockApollo(handlers),
+ provide: {
+ fullPath,
+ iid,
+ dataMethod: 'graphql',
+ graphqlResourceEtag: '/api/graphql:pipelines/id/320',
+ },
+ apolloProvider: createMockApolloProvider(handler),
}),
);
-
- return waitForPromises();
};
afterEach(() => {
wrapper.destroy();
});
- describe('linked pipelines', () => {
+ describe('loading state', () => {
+ it('should display loading state when loading', () => {
+ createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPipelineMiniGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('loaded state', () => {
beforeEach(async () => {
await createComponent();
});
- it('should display the mini pipeine graph', () => {
- expect(findMiniGraph().exists()).toBe(true);
+ it('should not display loading state after the query is resolved', async () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findPipelineMiniGraph().exists()).toBe(true);
});
- it('should not display linked pipelines', () => {
- expect(findUpstream().exists()).toBe(false);
- expect(findDownstream().exists()).toBe(false);
+ it('should display the pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(true);
});
});
- describe('when data is mismatched', () => {
- beforeEach(async () => {
- await createComponent({ props: { stages: [] } });
+ describe('load upstream/downstream', () => {
+ const samplePipeline = {
+ __typename: expect.any(String),
+ id: expect.any(String),
+ path: expect.any(String),
+ project: expect.any(Object),
+ detailedStatus: expect.any(Object),
+ };
+
+ it('formatted stages should be passed to the pipeline mini graph', async () => {
+ const stage = mockStages[0];
+ const expectedStages = [
+ {
+ name: stage.name,
+ status: {
+ __typename: 'DetailedStatus',
+ id: stage.status.id,
+ icon: stage.status.icon,
+ group: stage.status.group,
+ },
+ dropdown_path: stage.dropdown_path,
+ title: stage.title,
+ },
+ ];
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findPipelineMiniGraph().props('stages')).toEqual(expectedStages);
+ });
+
+ it('should render a downstream pipeline only', async () => {
+ createComponent(downstreamHandler);
+
+ await waitForPromises();
+
+ const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
+ const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
+
+ expect(downstreamPipelines).toEqual(expect.any(Array));
+ expect(upstreamPipeline).toEqual(null);
+ });
+
+ it('should pass the pipeline path prop for the counter badge', async () => {
+ createComponent(downstreamHandler);
+
+ await waitForPromises();
+
+ const expectedPath = mockDownstreamQueryResponse.data.project.pipeline.path;
+ const pipelinePath = findPipelineMiniGraph().props('pipelinePath');
+
+ expect(pipelinePath).toBe(expectedPath);
+ });
+
+ it('should render an upstream pipeline only', async () => {
+ createComponent(upstreamHandler);
+
+ await waitForPromises();
+
+ const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
+ const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
+
+ expect(upstreamPipeline).toEqual(samplePipeline);
+ expect(downstreamPipelines).toHaveLength(0);
});
- it('calls create flash with expected arguments', () => {
+ it('should render downstream and upstream pipelines', async () => {
+ createComponent(upstreamDownstreamHandler);
+
+ await waitForPromises();
+
+ const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
+ const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
+
+ expect(upstreamPipeline).toEqual(samplePipeline);
+ expect(downstreamPipelines).toEqual(expect.arrayContaining([samplePipeline]));
+ });
+ });
+
+ describe('error state', () => {
+ it('createFlash should show if there is an error fetching the data', async () => {
+ createComponent({ handler: failedHandler });
+
+ await waitForPromises();
+
expect(createFlash).toHaveBeenCalledWith({
- message: 'There was a problem handling the pipeline data.',
- captureError: true,
- error: new Error('Rest stages and graphQl stages must be the same length'),
+ message: 'There was a problem fetching linked pipelines.',
});
});
});
+
+ describe('polling', () => {
+ it('polling interval is set for linked pipelines', () => {
+ createComponent();
+
+ const expectedInterval = wrapper.vm.$apollo.queries.pipeline.options.pollInterval;
+
+ expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL);
+ });
+
+ it('polling interval is set for pipeline stages', () => {
+ createComponent();
+
+ const expectedInterval = wrapper.vm.$apollo.queries.pipelineStages.options.pollInterval;
+
+ expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL);
+ });
+
+ it('polls for stages and linked pipelines', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(stagesHandler).toHaveBeenCalledTimes(1);
+ expect(downstreamHandler).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch();
+ await waitForPromises();
+
+ expect(stagesHandler).toHaveBeenCalledTimes(2);
+ expect(downstreamHandler).toHaveBeenCalledTimes(2);
+
+ advanceToNextFetch();
+ await waitForPromises();
+
+ expect(stagesHandler).toHaveBeenCalledTimes(3);
+ expect(downstreamHandler).toHaveBeenCalledTimes(3);
+ });
+
+ it('toggles query polling with visibility check', async () => {
+ jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
+ wrapper.vm.$apollo.queries.pipelineStages,
+ );
+ expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
+ wrapper.vm.$apollo.queries.pipeline,
+ );
+ });
+ });
});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 43db6db00c1..73720c1cc88 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -37,9 +37,9 @@ describe('Commit pipeline status component', () => {
});
};
- const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findLink = () => wrapper.find('a');
- const findCiIcon = () => findLink().find(CiIcon);
+ const findCiIcon = () => findLink().findComponent(CiIcon);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index 8db162c07c2..aef137e6fa5 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -3,116 +3,21 @@ export const mockStages = [
name: 'build',
title: 'build: passed',
status: {
+ __typename: 'DetailedStatus',
+ id: 'success-409-409',
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
- details_path: '/root/ci-project/-/pipelines/611#build',
+ details_path: '/root/ci-project/-/pipelines/318#build',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
- path: '/root/ci-project/-/pipelines/611#build',
- dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=build',
- },
- {
- name: 'test',
- title: 'test: passed',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/root/ci-project/-/pipelines/611#test',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/root/ci-project/-/pipelines/611#test',
- dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test',
- },
- {
- name: 'test_two',
- title: 'test_two: passed',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/root/ci-project/-/pipelines/611#test_two',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/root/ci-project/-/pipelines/611#test_two',
- dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test_two',
- },
- {
- name: 'manual',
- title: 'manual: skipped',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/root/ci-project/-/pipelines/611#manual',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- action: {
- icon: 'play',
- title: 'Play all manual',
- path: '/root/ci-project/-/pipelines/611/stages/manual/play_manual',
- method: 'post',
- button_title: 'Play all manual',
- },
- },
- path: '/root/ci-project/-/pipelines/611#manual',
- dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=manual',
- },
- {
- name: 'deploy',
- title: 'deploy: passed',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/root/ci-project/-/pipelines/611#deploy',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/root/ci-project/-/pipelines/611#deploy',
- dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=deploy',
- },
- {
- name: 'qa',
- title: 'qa: passed',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/root/ci-project/-/pipelines/611#qa',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/root/ci-project/-/pipelines/611#qa',
- dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=qa',
+ path: '/root/ci-project/-/pipelines/318#build',
+ dropdown_path: '/root/ci-project/-/pipelines/318/stage.json?stage=build',
},
];
@@ -161,3 +66,109 @@ export const mockPipelineStatusResponse = {
},
},
};
+
+export const mockDownstreamQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ path: '/root/ci-project/-/pipelines/790',
+ id: 'pipeline-1',
+ downstream: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
+ detailedStatus: {
+ id: 'status-1',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+ },
+ upstream: null,
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockUpstreamDownstreamQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ id: 'pipeline-1',
+ path: '/root/ci-project/-/pipelines/790',
+ downstream: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
+ detailedStatus: {
+ id: 'status-1',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+ },
+ upstream: {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
+ detailedStatus: {
+ id: 'status-1',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockUpstreamQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ id: 'pipeline-1',
+ path: '/root/ci-project/-/pipelines/790',
+ downstream: {
+ nodes: [],
+ __typename: 'PipelineConnection',
+ },
+ upstream: {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
+ detailedStatus: {
+ id: 'status-1',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 71ee12cf02d..d89a238105b 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -302,6 +302,33 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(findModal()).not.toBeNull();
});
});
+
+ describe('when no pipelines were created on a forked merge request', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(200, []);
+
+ createComponent({
+ projectId: '5',
+ mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
+ });
+
+ jest.spyOn(findModal().vm, 'show').mockReturnValue();
+
+ await waitForPromises();
+ });
+
+ it('should show security modal from empty state run pipeline button', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findModal().exists()).toBe(true);
+
+ findRunPipelineBtn().trigger('click');
+
+ expect(findModal().vm.show).toHaveBeenCalled();
+ });
+ });
});
describe('unsuccessfull request', () => {
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
index 14a0b98a0d5..770f2636648 100644
--- a/spec/frontend/confidential_merge_request/components/dropdown_spec.js
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -30,18 +30,18 @@ describe('Confidential merge request project dropdown component', () => {
},
]);
- expect(vm.findAll(GlDropdownItem).length).toBe(2);
+ expect(vm.findAllComponents(GlDropdownItem).length).toBe(2);
});
it('shows lock icon', () => {
factory();
- expect(vm.find(GlDropdown).props('icon')).toBe('lock');
+ expect(vm.findComponent(GlDropdown).props('icon')).toBe('lock');
});
it('has dropdown text', () => {
factory();
- expect(vm.find(GlDropdown).props('text')).toBe('Select private project');
+ expect(vm.findComponent(GlDropdown).props('text')).toBe('Select private project');
});
});
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
index b54f7cf17c8..6ad8a9de8d3 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -1,49 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`content_editor/components/toolbar_link_button renders dropdown component 1`] = `
-"<div class=\\"dropdown b-dropdown gl-new-dropdown btn-group\\" aria-label=\\"Insert link\\" title=\\"Insert link\\">
- <!----><button aria-haspopup=\\"true\\" aria-expanded=\\"false\\" type=\\"button\\" class=\\"btn dropdown-toggle btn-default btn-sm gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only\\">
- <!----> <svg data-testid=\\"link-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"dropdown-icon gl-icon s16\\">
- <use href=\\"#link\\"></use>
- </svg> <span class=\\"gl-new-dropdown-button-text\\"></span> <svg data-testid=\\"chevron-down-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-button-icon dropdown-chevron gl-icon s16\\">
- <use href=\\"#chevron-down\\"></use>
- </svg></button>
- <ul role=\\"menu\\" tabindex=\\"-1\\" class=\\"dropdown-menu\\">
- <div class=\\"gl-new-dropdown-inner\\">
+"<div title=\\"Insert link\\" lazy=\\"\\">
+ <li role=\\"presentation\\" class=\\"gl-px-3!\\">
+ <form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
+ <div role=\\"group\\" class=\\"input-group\\" placeholder=\\"Link URL\\">
+ <!---->
+ <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"form-control gl-form-input\\">
+ <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
+ <!---->
+ <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
+ <!---->
+ </div>
+ </form>
+ </li>
+ <li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\">
+ <hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
+ </li>
+ <li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
+ <!---->
<!---->
<!---->
- <div class=\\"gl-new-dropdown-contents\\">
+ <div class=\\"gl-new-dropdown-item-text-wrapper\\">
+ <p class=\\"gl-new-dropdown-item-text-primary\\">
+ Upload file
+ </p>
<!---->
- <li role=\\"presentation\\" class=\\"gl-px-3!\\">
- <form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
- <div role=\\"group\\" class=\\"input-group\\" placeholder=\\"Link URL\\">
- <!---->
- <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"form-control gl-form-input\\">
- <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
- <!---->
- </div>
- </form>
- </li>
- <li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\">
- <hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
- </li>
- <li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
- <!---->
- <!---->
- <!---->
- <div class=\\"gl-new-dropdown-item-text-wrapper\\">
- <p class=\\"gl-new-dropdown-item-text-primary\\">
- Upload file
- </p>
- <!---->
- </div>
- <!---->
- </button></li> <input type=\\"file\\" name=\\"content_editor_attachment\\" class=\\"gl-display-none\\">
</div>
<!---->
- </div>
- </ul>
+ </button></li>
</div>"
`;
diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
new file mode 100644
index 00000000000..0700cf5d529
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
@@ -0,0 +1,126 @@
+import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import { createTestEditor } from '../../test_utils';
+
+jest.mock('@tiptap/extension-bubble-menu');
+
+describe('content_editor/components/bubble_menus/bubble_menu', () => {
+ let wrapper;
+ let tiptapEditor;
+ const pluginKey = 'key';
+ const shouldShow = jest.fn();
+ const tippyOptions = { placement: 'bottom' };
+ const pluginInitializationResult = {};
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor();
+ };
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = shallowMountExtended(BubbleMenu, {
+ provide: {
+ tiptapEditor,
+ },
+ propsData: {
+ pluginKey,
+ shouldShow,
+ tippyOptions,
+ ...propsData,
+ },
+ slots: {
+ default: '<div>menu content</div>',
+ },
+ });
+ };
+
+ const setupMocks = () => {
+ BubbleMenuPlugin.mockReturnValueOnce(pluginInitializationResult);
+ jest.spyOn(tiptapEditor, 'registerPlugin').mockImplementationOnce(() => true);
+ };
+
+ const invokeTippyEvent = (eventName, eventArgs) => {
+ const pluginConfig = BubbleMenuPlugin.mock.calls[0][0];
+
+ pluginConfig.tippyOptions[eventName](eventArgs);
+ };
+
+ beforeEach(() => {
+ buildEditor();
+ setupMocks();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('initializes BubbleMenuPlugin', async () => {
+ createWrapper({});
+
+ await nextTick();
+
+ expect(BubbleMenuPlugin).toHaveBeenCalledWith({
+ pluginKey,
+ editor: tiptapEditor,
+ shouldShow,
+ element: wrapper.vm.$el,
+ tippyOptions: expect.objectContaining({
+ onHidden: expect.any(Function),
+ onShow: expect.any(Function),
+ ...tippyOptions,
+ }),
+ });
+
+ expect(tiptapEditor.registerPlugin).toHaveBeenCalledWith(pluginInitializationResult);
+ });
+
+ it('does not render default slot by default', async () => {
+ createWrapper({});
+
+ await nextTick();
+
+ expect(wrapper.text()).not.toContain('menu content');
+ });
+
+ describe('when onShow event handler is invoked', () => {
+ const onShowArgs = {};
+
+ beforeEach(async () => {
+ createWrapper({});
+
+ await nextTick();
+
+ invokeTippyEvent('onShow', onShowArgs);
+ });
+
+ it('displays the menu content', () => {
+ expect(wrapper.text()).toContain('menu content');
+ });
+
+ it('emits show event', () => {
+ expect(wrapper.emitted('show')).toEqual([[onShowArgs]]);
+ });
+ });
+
+ describe('when onHidden event handler is invoked', () => {
+ const onHiddenArgs = {};
+
+ beforeEach(async () => {
+ createWrapper({});
+
+ await nextTick();
+
+ invokeTippyEvent('onShow', onHiddenArgs);
+ invokeTippyEvent('onHidden', onHiddenArgs);
+ });
+
+ it('displays the menu content', () => {
+ expect(wrapper.text()).not.toContain('menu content');
+ });
+
+ it('emits show event', () => {
+ expect(wrapper.emitted('hidden')).toEqual([[onHiddenArgs]]);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
index 154035a46ed..378b11f4ae9 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
@@ -1,4 +1,3 @@
-import { BubbleMenu } from '@tiptap/vue-2';
import {
GlDropdown,
GlDropdownForm,
@@ -9,8 +8,9 @@ import {
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
-import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue';
+import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import Diagram from '~/content_editor/extensions/diagram';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
@@ -18,7 +18,7 @@ import { createTestEditor, emitEditorEvent } from '../../test_utils';
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
-describe('content_editor/components/bubble_menus/code_block', () => {
+describe('content_editor/components/bubble_menus/code_block_bubble_menu', () => {
let wrapper;
let tiptapEditor;
let contentEditor;
@@ -40,6 +40,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
},
stubs: {
GlDropdownItem: stubComponent(GlDropdownItem),
+ BubbleMenu: stubComponent(BubbleMenu),
},
});
};
@@ -73,7 +74,6 @@ describe('content_editor/components/bubble_menus/code_block', () => {
await emitEditorEvent({ event: 'transaction', tiptapEditor });
- expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
index 1e2f58d9e40..cce17176129 100644
--- a/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
@@ -1,7 +1,8 @@
-import { BubbleMenu } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue';
+import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting_bubble_menu.vue';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import { stubComponent } from 'helpers/stub_component';
import {
BUBBLE_MENU_TRACKING_ACTION,
@@ -9,7 +10,7 @@ import {
} from '~/content_editor/constants';
import { createTestEditor } from '../../test_utils';
-describe('content_editor/components/bubble_menus/formatting', () => {
+describe('content_editor/components/bubble_menus/formatting_bubble_menu', () => {
let wrapper;
let trackingSpy;
let tiptapEditor;
@@ -25,6 +26,9 @@ describe('content_editor/components/bubble_menus/formatting', () => {
provide: {
tiptapEditor,
},
+ stubs: {
+ BubbleMenu: stubComponent(BubbleMenu),
+ },
});
};
@@ -41,7 +45,6 @@ describe('content_editor/components/bubble_menus/formatting', () => {
buildWrapper();
const bubbleMenu = wrapper.findComponent(BubbleMenu);
- expect(bubbleMenu.props().editor).toBe(tiptapEditor);
expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
index 93204deb68c..9aa9c6483f4 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
@@ -1,18 +1,20 @@
import { GlLink, GlForm } from '@gitlab/ui';
-import { BubbleMenu } from '@tiptap/vue-2';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue';
+import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
+import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import { stubComponent } from 'helpers/stub_component';
import Link from '~/content_editor/extensions/link';
-import { createTestEditor, emitEditorEvent } from '../../test_utils';
+import { createTestEditor } from '../../test_utils';
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
-describe('content_editor/components/bubble_menus/link', () => {
+describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
let wrapper;
let tiptapEditor;
let contentEditor;
- let bubbleMenu;
let eventHub;
const buildEditor = () => {
@@ -28,9 +30,28 @@ describe('content_editor/components/bubble_menus/link', () => {
contentEditor,
eventHub,
},
+ stubs: {
+ BubbleMenu: stubComponent(BubbleMenu),
+ },
});
};
+ const showMenu = () => {
+ wrapper.findComponent(BubbleMenu).vm.$emit('show');
+ return nextTick();
+ };
+
+ const buildWrapperAndDisplayMenu = () => {
+ buildWrapper();
+
+ return showMenu();
+ };
+
+ const findBubbleMenu = () => wrapper.findComponent(BubbleMenu);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
+ const findEditLinkButton = () => wrapper.findByTestId('edit-link');
+
const expectLinkButtonsToExist = (exist = true) => {
expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
expect(wrapper.findByTestId('copy-link-url').exists()).toBe(exist);
@@ -40,7 +61,6 @@ describe('content_editor/components/bubble_menus/link', () => {
beforeEach(async () => {
buildEditor();
- buildWrapper();
tiptapEditor
.chain()
@@ -49,10 +69,6 @@ describe('content_editor/components/bubble_menus/link', () => {
)
.setTextSelection(14) // put cursor in the middle of the link
.run();
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor });
-
- bubbleMenu = wrapper.findComponent(BubbleMenu);
});
afterEach(() => {
@@ -60,13 +76,15 @@ describe('content_editor/components/bubble_menus/link', () => {
});
it('renders bubble menu component', async () => {
- expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ await buildWrapperAndDisplayMenu();
+
+ expect(findBubbleMenu().classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
it('shows a clickable link to the URL in the link node', async () => {
- const link = wrapper.findComponent(GlLink);
- expect(link.attributes()).toEqual(
+ await buildWrapperAndDisplayMenu();
+
+ expect(findLink().attributes()).toEqual(
expect.objectContaining({
href: '/path/to/project/-/wikis/uploads/my_file.pdf',
'aria-label': 'uploads/my_file.pdf',
@@ -74,11 +92,82 @@ describe('content_editor/components/bubble_menus/link', () => {
target: '_blank',
}),
);
- expect(link.text()).toBe('uploads/my_file.pdf');
+ expect(findLink().text()).toBe('uploads/my_file.pdf');
+ });
+
+ it('updates the bubble menu state when @selectionUpdate event is triggered', async () => {
+ const linkUrl = 'https://gitlab.com';
+
+ await buildWrapperAndDisplayMenu();
+
+ expect(findLink().attributes()).toEqual(
+ expect.objectContaining({
+ href: '/path/to/project/-/wikis/uploads/my_file.pdf',
+ }),
+ );
+
+ tiptapEditor
+ .chain()
+ .setContent(
+ `Link to <a href="${linkUrl}" data-canonical-src="${linkUrl}" title="Click here to download">GitLab</a>`,
+ )
+ .setTextSelection(11)
+ .run();
+
+ findEditorStateObserver().vm.$emit('selectionUpdate');
+
+ await nextTick();
+
+ expect(findLink().attributes()).toEqual(
+ expect.objectContaining({
+ href: linkUrl,
+ }),
+ );
+ });
+
+ describe('when the selection changes within the same link', () => {
+ it('does not update the bubble menu state', async () => {
+ await buildWrapperAndDisplayMenu();
+
+ await findEditLinkButton().trigger('click');
+
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+
+ tiptapEditor.commands.setTextSelection(13);
+
+ findEditorStateObserver().vm.$emit('selectionUpdate');
+
+ await nextTick();
+
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+ });
+ });
+
+ it('cleans bubble menu state when hidden event is triggered', async () => {
+ await buildWrapperAndDisplayMenu();
+
+ expect(findLink().attributes()).toEqual(
+ expect.objectContaining({
+ href: '/path/to/project/-/wikis/uploads/my_file.pdf',
+ }),
+ );
+
+ findBubbleMenu().vm.$emit('hidden');
+
+ await nextTick();
+
+ expect(findLink().attributes()).toEqual(
+ expect.objectContaining({
+ href: '#',
+ }),
+ );
+ expect(findLink().text()).toEqual('');
});
describe('copy button', () => {
it('copies the canonical link to clipboard', async () => {
+ await buildWrapperAndDisplayMenu();
+
jest.spyOn(navigator.clipboard, 'writeText');
await wrapper.findByTestId('copy-link-url').vm.$emit('click');
@@ -89,6 +178,7 @@ describe('content_editor/components/bubble_menus/link', () => {
describe('remove link button', () => {
it('removes the link', async () => {
+ await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('remove-link').vm.$emit('click');
expect(tiptapEditor.getHTML()).toBe('<p>Download PDF File</p>');
@@ -106,7 +196,7 @@ describe('content_editor/components/bubble_menus/link', () => {
.setTextSelection(4)
.run();
- await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ await buildWrapperAndDisplayMenu();
});
it('directly opens the edit form for a placeholder link', async () => {
@@ -133,6 +223,7 @@ describe('content_editor/components/bubble_menus/link', () => {
let linkTitleInput;
beforeEach(async () => {
+ await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('edit-link').vm.$emit('click');
linkHrefInput = wrapper.findByTestId('link-href');
@@ -157,19 +248,6 @@ describe('content_editor/components/bubble_menus/link', () => {
expect(to).toBe(18);
});
- it('shows the copy/edit/remove link buttons again if selection changes to another non-link and then back again to a link', async () => {
- expectLinkButtonsToExist(false);
-
- tiptapEditor.commands.setTextSelection(3);
- await emitEditorEvent({ event: 'transaction', tiptapEditor });
-
- tiptapEditor.commands.setTextSelection(14);
- await emitEditorEvent({ event: 'transaction', tiptapEditor });
-
- expectLinkButtonsToExist(true);
- expect(wrapper.findComponent(GlForm).exists()).toBe(false);
- });
-
describe('after making changes in the form and clicking apply', () => {
beforeEach(async () => {
linkHrefInput.setValue('https://google.com');
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
index fada4f06743..13c6495ac41 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
@@ -1,7 +1,8 @@
import { GlLink, GlForm } from '@gitlab/ui';
-import { BubbleMenu } from '@tiptap/vue-2';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media.vue';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
+import { stubComponent } from 'helpers/stub_component';
import eventHubFactory from '~/helpers/event_hub_factory';
import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
@@ -33,7 +34,7 @@ describe.each`
${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
`(
- 'content_editor/components/bubble_menus/media ($mediaType)',
+ 'content_editor/components/bubble_menus/media_bubble_menu ($mediaType)',
({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
let wrapper;
let tiptapEditor;
@@ -54,11 +55,14 @@ describe.each`
contentEditor,
eventHub,
},
+ stubs: {
+ BubbleMenu: stubComponent(BubbleMenu),
+ },
});
};
const selectFile = async (file) => {
- const input = wrapper.find({ ref: 'fileSelector' });
+ const input = wrapper.findComponent({ ref: 'fileSelector' });
// override the property definition because `input.files` isn't directly modifyable
Object.defineProperty(input.element, 'files', { value: [file], writable: true });
@@ -94,7 +98,6 @@ describe.each`
});
it('renders bubble menu component', async () => {
- expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index 12484cb13c6..ee9ead8f8a7 100644
--- a/spec/frontend/content_editor/components/content_editor_alert_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -51,6 +51,16 @@ describe('content_editor/components/content_editor_alert', () => {
},
);
+ it('does not show primary action by default', async () => {
+ const message = 'error message';
+
+ createWrapper();
+ eventHub.$emit(ALERT_EVENT, { message });
+ await nextTick();
+
+ expect(findErrorAlert().attributes().primaryButtonText).toBeUndefined();
+ });
+
it('allows dismissing the error', async () => {
const message = 'error message';
@@ -62,4 +72,19 @@ describe('content_editor/components/content_editor_alert', () => {
expect(findErrorAlert().exists()).toBe(false);
});
+
+ it('allows dismissing the error with a primary action button', async () => {
+ const message = 'error message';
+ const actionLabel = 'Retry';
+ const action = jest.fn();
+
+ createWrapper();
+ eventHub.$emit(ALERT_EVENT, { message, action, actionLabel });
+ await nextTick();
+ findErrorAlert().vm.$emit('primaryAction');
+ await nextTick();
+
+ expect(action).toHaveBeenCalled();
+ expect(findErrorAlert().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 0ba2672100b..ae52cb05eaf 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,136 +1,227 @@
-import { EditorContent } from '@tiptap/vue-2';
+import { GlAlert } from '@gitlab/ui';
+import { EditorContent, Editor } from '@tiptap/vue-2';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue';
+import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting_bubble_menu.vue';
+import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
+import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
+import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
-import { emitEditorEvent } from '../test_utils';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji');
describe('ContentEditor', () => {
let wrapper;
- let contentEditor;
let renderMarkdown;
const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
- const createWrapper = (propsData = {}) => {
- renderMarkdown = jest.fn();
-
+ const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
+ const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
+ const createWrapper = ({ markdown } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
- ...propsData,
+ markdown,
},
stubs: {
EditorStateObserver,
ContentEditorProvider,
- },
- listeners: {
- initialized(editor) {
- contentEditor = editor;
- },
+ ContentEditorAlert,
},
});
};
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- it('triggers initialized event and provides contentEditor instance as event data', () => {
+ it('triggers initialized event', () => {
createWrapper();
- expect(contentEditor).not.toBeFalsy();
+ expect(wrapper.emitted('initialized')).toHaveLength(1);
});
- it('renders EditorContent component and provides tiptapEditor instance', () => {
- createWrapper();
+ it('renders EditorContent component and provides tiptapEditor instance', async () => {
+ const markdown = 'hello world';
+
+ createWrapper({ markdown });
+
+ renderMarkdown.mockResolvedValueOnce(markdown);
+
+ await nextTick();
const editorContent = findEditorContent();
- expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor);
+ expect(editorContent.props().editor).toBeInstanceOf(Editor);
expect(editorContent.classes()).toContain('md');
});
- it('renders ContentEditorProvider component', () => {
- createWrapper();
+ it('renders ContentEditorProvider component', async () => {
+ await createWrapper();
expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
- it('renders top toolbar component', () => {
- createWrapper();
+ it('renders top toolbar component', async () => {
+ await createWrapper();
expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
});
- it('adds is-focused class when focus event is emitted', async () => {
- createWrapper();
+ describe('when setting initial content', () => {
+ it('displays loading indicator', async () => {
+ createWrapper();
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
+ await nextTick();
- expect(findEditorElement().classes()).toContain('is-focused');
- });
+ expect(findLoadingIndicator().exists()).toBe(true);
+ });
- it('removes is-focused class when blur event is emitted', async () => {
- createWrapper();
+ it('emits loading event', async () => {
+ createWrapper();
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' });
+ await nextTick();
- expect(findEditorElement().classes()).not.toContain('is-focused');
- });
+ expect(wrapper.emitted('loading')).toHaveLength(1);
+ });
- it('emits change event when document is updated', async () => {
- createWrapper();
+ describe('succeeds', () => {
+ beforeEach(async () => {
+ renderMarkdown.mockResolvedValueOnce('hello world');
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' });
+ createWrapper({ markddown: 'hello world' });
+ await nextTick();
+ });
- expect(wrapper.emitted('change')).toEqual([
- [
- {
- empty: contentEditor.empty,
- },
- ],
- ]);
- });
+ it('hides loading indicator', async () => {
+ await nextTick();
+ expect(findLoadingIndicator().exists()).toBe(false);
+ });
- it('renders content_editor_alert component', () => {
- createWrapper();
+ it('emits loadingSuccess event', () => {
+ expect(wrapper.emitted('loadingSuccess')).toHaveLength(1);
+ });
+ });
+
+ describe('fails', () => {
+ beforeEach(async () => {
+ renderMarkdown.mockRejectedValueOnce(new Error());
+
+ createWrapper({ markddown: 'hello world' });
+ await nextTick();
+ });
+
+ it('sets the content editor as read only when loading content fails', async () => {
+ await nextTick();
- expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true);
+ expect(findEditorContent().props().editor.isEditable).toBe(false);
+ });
+
+ it('hides loading indicator', async () => {
+ await nextTick();
+
+ expect(findLoadingIndicator().exists()).toBe(false);
+ });
+
+ it('emits loadingError event', () => {
+ expect(wrapper.emitted('loadingError')).toHaveLength(1);
+ });
+
+ it('displays error alert indicating that the content editor failed to load', () => {
+ expect(findContentEditorAlert().text()).toContain(
+ 'An error occurred while trying to render the content editor. Please try again.',
+ );
+ });
+
+ describe('when clicking the retry button in the loading error alert and loading succeeds', () => {
+ beforeEach(async () => {
+ renderMarkdown.mockResolvedValueOnce('hello markdown');
+ await wrapper.findComponent(GlAlert).vm.$emit('primaryAction');
+ });
+
+ it('hides the loading error alert', () => {
+ expect(findContentEditorAlert().text()).toBe('');
+ });
+
+ it('sets the content editor as writable', async () => {
+ await nextTick();
+
+ expect(findEditorContent().props().editor.isEditable).toBe(true);
+ });
+ });
+ });
});
- it('renders loading indicator component', () => {
- createWrapper();
+ describe('when focused event is emitted', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit('focus');
+
+ await nextTick();
+ });
- expect(wrapper.findComponent(LoadingIndicator).exists()).toBe(true);
+ it('adds is-focused class when focus event is emitted', () => {
+ expect(findEditorElement().classes()).toContain('is-focused');
+ });
+
+ it('removes is-focused class when blur event is emitted', async () => {
+ findEditorStateObserver().vm.$emit('blur');
+
+ await nextTick();
+
+ expect(findEditorElement().classes()).not.toContain('is-focused');
+ });
});
- it('renders formatting bubble menu', () => {
- createWrapper();
+ describe('when editorStateObserver emits docUpdate event', () => {
+ it('emits change event with the latest markdown', async () => {
+ const markdown = 'Loaded content';
- expect(wrapper.findComponent(FormattingBubbleMenu).exists()).toBe(true);
+ renderMarkdown.mockResolvedValueOnce(markdown);
+
+ createWrapper({ markdown: 'initial content' });
+
+ await nextTick();
+ await waitForPromises();
+
+ findEditorStateObserver().vm.$emit('docUpdate');
+
+ expect(wrapper.emitted('change')).toEqual([
+ [
+ {
+ markdown,
+ changed: false,
+ empty: false,
+ },
+ ],
+ ]);
+ });
});
it.each`
- event
- ${'loading'}
- ${'loadingSuccess'}
- ${'loadingError'}
- `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => {
+ name | component
+ ${'formatting'} | ${FormattingBubbleMenu}
+ ${'link'} | ${LinkBubbleMenu}
+ ${'media'} | ${MediaBubbleMenu}
+ ${'codeBlock'} | ${CodeBlockBubbleMenu}
+ `('renders formatting bubble menu', ({ component }) => {
createWrapper();
- findEditorStateObserver().vm.$emit(event);
-
- expect(wrapper.emitted(event)).toHaveLength(1);
+ expect(wrapper.findComponent(component).exists()).toBe(true);
});
});
diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js
index 51a594a606b..e8c2d8c8793 100644
--- a/spec/frontend/content_editor/components/editor_state_observer_spec.js
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -4,12 +4,7 @@ import EditorStateObserver, {
tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
- ALERT_EVENT,
-} from '~/content_editor/constants';
+import { ALERT_EVENT } from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => {
@@ -18,9 +13,6 @@ describe('content_editor/components/editor_state_observer', () => {
let onDocUpdateListener;
let onSelectionUpdateListener;
let onTransactionListener;
- let onLoadingContentListener;
- let onLoadingSuccessListener;
- let onLoadingErrorListener;
let onAlertListener;
let eventHub;
@@ -38,9 +30,6 @@ describe('content_editor/components/editor_state_observer', () => {
selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener,
[ALERT_EVENT]: onAlertListener,
- [LOADING_CONTENT_EVENT]: onLoadingContentListener,
- [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener,
- [LOADING_ERROR_EVENT]: onLoadingErrorListener,
},
});
};
@@ -50,9 +39,6 @@ describe('content_editor/components/editor_state_observer', () => {
onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn();
onAlertListener = jest.fn();
- onLoadingSuccessListener = jest.fn();
- onLoadingContentListener = jest.fn();
- onLoadingErrorListener = jest.fn();
buildEditor();
});
@@ -81,11 +67,8 @@ describe('content_editor/components/editor_state_observer', () => {
});
it.each`
- event | listener
- ${ALERT_EVENT} | ${() => onAlertListener}
- ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener}
- ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener}
- ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener}
+ event | listener
+ ${ALERT_EVENT} | ${() => onAlertListener}
`('listens to $event event in the eventBus object', ({ event, listener }) => {
const args = {};
@@ -114,9 +97,6 @@ describe('content_editor/components/editor_state_observer', () => {
it.each`
event
${ALERT_EVENT}
- ${LOADING_CONTENT_EVENT}
- ${LOADING_SUCCESS_EVENT}
- ${LOADING_ERROR_EVENT}
`('removes $event event hook from eventHub', ({ event }) => {
jest.spyOn(eventHub, '$off');
jest.spyOn(eventHub, '$on');
diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js
index e4fb09b70a4..0065103d01b 100644
--- a/spec/frontend/content_editor/components/loading_indicator_spec.js
+++ b/spec/frontend/content_editor/components/loading_indicator_spec.js
@@ -1,18 +1,10 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
-import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
-} from '~/content_editor/constants';
describe('content_editor/components/loading_indicator', () => {
let wrapper;
- const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createWrapper = () => {
@@ -24,48 +16,12 @@ describe('content_editor/components/loading_indicator', () => {
});
describe('when loading content', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createWrapper();
-
- findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
-
- await nextTick();
});
it('displays loading indicator', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
-
- describe('when loading content succeeds', () => {
- beforeEach(async () => {
- createWrapper();
-
- findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
- await nextTick();
- findEditorStateObserver().vm.$emit(LOADING_SUCCESS_EVENT);
- await nextTick();
- });
-
- it('hides loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
- });
-
- describe('when loading content fails', () => {
- const error = 'error';
-
- beforeEach(async () => {
- createWrapper();
-
- findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
- await nextTick();
- findEditorStateObserver().vm.$emit(LOADING_ERROR_EVENT, error);
- await nextTick();
- });
-
- it('hides loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
index dab7e67d7c5..5473d43f5a1 100644
--- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlFormInputGroup } from '@gitlab/ui';
+import { GlButton, GlFormInputGroup, GlDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
import Attachment from '~/content_editor/extensions/attachment';
import Image from '~/content_editor/extensions/image';
+import { stubComponent } from 'helpers/stub_component';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_image_button', () => {
@@ -14,15 +15,19 @@ describe('content_editor/components/toolbar_image_button', () => {
provide: {
tiptapEditor: editor,
},
+ stubs: {
+ GlDropdown: stubComponent(GlDropdown),
+ },
});
};
const findImageURLInput = () =>
wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
const findApplyImageButton = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const selectFile = async (file) => {
- const input = wrapper.find({ ref: 'fileSelector' });
+ const input = wrapper.findComponent({ ref: 'fileSelector' });
// override the property definition because `input.files` isn't directly modifyable
Object.defineProperty(input.element, 'files', { value: [file], writable: true });
@@ -77,4 +82,16 @@ describe('content_editor/components/toolbar_image_button', () => {
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
});
+
+ describe('a11y tests', () => {
+ it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
+ buildWrapper();
+
+ expect(findDropdown().props()).toMatchObject({
+ text: 'Insert image',
+ textSrOnly: true,
+ });
+ expect(findDropdown().attributes('title')).toBe('Insert image');
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
index fc26a9da471..40e859e96af 100644
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -4,6 +4,7 @@ import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.v
import eventHubFactory from '~/helpers/event_hub_factory';
import Link from '~/content_editor/extensions/link';
import { hasSelection } from '~/content_editor/services/utils';
+import { stubComponent } from 'helpers/stub_component';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
jest.mock('~/content_editor/services/utils');
@@ -18,6 +19,9 @@ describe('content_editor/components/toolbar_link_button', () => {
tiptapEditor: editor,
eventHub: eventHubFactory(),
},
+ stubs: {
+ GlDropdown: stubComponent(GlDropdown),
+ },
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
@@ -26,7 +30,7 @@ describe('content_editor/components/toolbar_link_button', () => {
const findRemoveLinkButton = () => wrapper.findByText('Remove link');
const selectFile = async (file) => {
- const input = wrapper.find({ ref: 'fileSelector' });
+ const input = wrapper.findComponent({ ref: 'fileSelector' });
// override the property definition because `input.files` isn't directly modifyable
Object.defineProperty(input.element, 'files', { value: [file], writable: true });
@@ -205,4 +209,16 @@ describe('content_editor/components/toolbar_link_button', () => {
});
});
});
+
+ describe('a11y tests', () => {
+ it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
+ buildWrapper();
+
+ expect(findDropdown().props()).toMatchObject({
+ text: 'Insert link',
+ textSrOnly: true,
+ });
+ expect(findDropdown().attributes('title')).toBe('Insert link');
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
index 62fec8d4e72..a23f8370adf 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -1,8 +1,10 @@
+import { GlDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue';
import Diagram from '~/content_editor/extensions/diagram';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
import eventHubFactory from '~/helpers/event_hub_factory';
+import { stubComponent } from 'helpers/stub_component';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_more_dropdown', () => {
@@ -23,10 +25,15 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
tiptapEditor,
eventHub,
},
+ stubs: {
+ GlDropdown: stubComponent(GlDropdown),
+ },
propsData,
});
};
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
beforeEach(() => {
buildEditor();
buildWrapper();
@@ -67,4 +74,14 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
expect(wrapper.emitted('execute')).toEqual([[{ contentType }]]);
});
});
+
+ describe('a11y tests', () => {
+ it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
+ expect(findDropdown().props()).toMatchObject({
+ text: 'More',
+ textSrOnly: true,
+ });
+ expect(findDropdown().attributes('title')).toBe('More');
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_table_button_spec.js b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
index 056e5e04e1f..aa4604661e5 100644
--- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue';
+import { stubComponent } from 'helpers/stub_component';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_table_button', () => {
@@ -12,6 +13,9 @@ describe('content_editor/components/toolbar_table_button', () => {
provide: {
tiptapEditor: editor,
},
+ stubs: {
+ GlDropdown: stubComponent(GlDropdown),
+ },
});
};
@@ -98,4 +102,14 @@ describe('content_editor/components/toolbar_table_button', () => {
expect(getNumButtons()).toBe(100); // 10x10 (and not 11x11)
});
+
+ describe('a11y tests', () => {
+ it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
+ expect(findDropdown().props()).toMatchObject({
+ text: 'Insert table',
+ textSrOnly: true,
+ });
+ expect(findDropdown().attributes('title')).toBe('Insert table');
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
index 608be1bd693..3ebb305afbf 100644
--- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -53,7 +53,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
});
});
- describe('when there is an active item ', () => {
+ describe('when there is an active item', () => {
let activeTextStyle;
beforeEach(async () => {
@@ -68,7 +68,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
await emitEditorEvent({ event: 'transaction', tiptapEditor });
});
- it('displays the active text style label as the dropdown toggle text ', () => {
+ it('displays the active text style label as the dropdown toggle text', () => {
expect(findDropdown().props().text).toBe(activeTextStyle.label);
});
diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index 17a365e12bb..a5ef19fb8e8 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -104,7 +104,7 @@ describe('content/components/wrappers/code_block', () => {
it('does not render a preview if showPreview: false', async () => {
createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false });
- expect(wrapper.find({ ref: 'diagramContainer' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'diagramContainer' }).exists()).toBe(false);
});
it('does not update preview when diagram is not active', async () => {
@@ -134,7 +134,7 @@ describe('content/components/wrappers/code_block', () => {
await nextTick();
expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram');
- expect(wrapper.find(SandboxedMermaid).exists()).toBe(false);
+ expect(wrapper.findComponent(SandboxedMermaid).exists()).toBe(false);
});
it('renders an iframe with preview for a mermaid diagram', async () => {
@@ -143,7 +143,7 @@ describe('content/components/wrappers/code_block', () => {
await emitEditorEvent({ event: 'transaction', tiptapEditor });
await nextTick();
- expect(wrapper.find(SandboxedMermaid).props('source')).toBe('');
+ expect(wrapper.findComponent(SandboxedMermaid).props('source')).toBe('');
expect(wrapper.find('img').exists()).toBe(false);
});
});
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
index 53efda6aee2..30e798e8817 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
@@ -5,12 +5,7 @@ import Frontmatter from '~/content_editor/extensions/frontmatter';
import Bold from '~/content_editor/extensions/bold';
import { VARIANT_DANGER } from '~/flash';
import eventHubFactory from '~/helpers/event_hub_factory';
-import {
- ALERT_EVENT,
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
-} from '~/content_editor/constants';
+import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
@@ -115,13 +110,6 @@ describe('content_editor/extensions/paste_markdown', () => {
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
-
- it(`triggers ${LOADING_SUCCESS_EVENT}`, async () => {
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
-
- expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_CONTENT_EVENT);
- expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_SUCCESS_EVENT);
- });
});
describe('when rendering markdown fails', () => {
@@ -129,13 +117,6 @@ describe('content_editor/extensions/paste_markdown', () => {
renderMarkdown.mockRejectedValueOnce();
});
- it(`triggers ${LOADING_ERROR_EVENT} event`, async () => {
- await triggerPasteEventHandler(buildClipboardEvent());
- await waitForPromises();
-
- expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_ERROR_EVENT);
- });
-
it(`triggers ${ALERT_EVENT} event`, async () => {
await triggerPasteEventHandler(buildClipboardEvent());
await waitForPromises();
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 7ae0a7c13c1..bc43af9bd8b 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -1,8 +1,10 @@
+import Audio from '~/content_editor/extensions/audio';
import Bold from '~/content_editor/extensions/bold';
import Blockquote from '~/content_editor/extensions/blockquote';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Diagram from '~/content_editor/extensions/diagram';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
import Frontmatter from '~/content_editor/extensions/frontmatter';
@@ -21,22 +23,27 @@ import Sourcemap from '~/content_editor/extensions/sourcemap';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
import TableHeader from '~/content_editor/extensions/table_header';
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
import TableRow from '~/content_editor/extensions/table_row';
import TableCell from '~/content_editor/extensions/table_cell';
import TaskList from '~/content_editor/extensions/task_list';
import TaskItem from '~/content_editor/extensions/task_item';
+import Video from '~/content_editor/extensions/video';
import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import { SAFE_VIDEO_EXT, SAFE_AUDIO_EXT, DIAGRAM_LANGUAGES } from '~/content_editor/constants';
import { createTestEditor, createDocBuilder } from './test_utils';
const tiptapEditor = createTestEditor({
extensions: [
+ Audio,
Blockquote,
Bold,
BulletList,
Code,
CodeBlockHighlight,
+ Diagram,
FootnoteDefinition,
FootnoteReference,
Frontmatter,
@@ -55,8 +62,10 @@ const tiptapEditor = createTestEditor({
TableRow,
TableHeader,
TableCell,
+ TableOfContents,
TaskList,
TaskItem,
+ Video,
...HTMLNodes,
],
});
@@ -65,12 +74,14 @@ const {
builders: {
doc,
paragraph,
+ audio,
bold,
blockquote,
bulletList,
code,
codeBlock,
div,
+ diagram,
footnoteDefinition,
footnoteReference,
frontmatter,
@@ -89,17 +100,21 @@ const {
tableRow,
tableHeader,
tableCell,
+ tableOfContents,
taskItem,
taskList,
+ video,
},
} = createDocBuilder({
tiptapEditor,
names: {
+ audio: { nodeType: Audio.name },
blockquote: { nodeType: Blockquote.name },
bold: { markType: Bold.name },
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ diagram: { nodeType: Diagram.name },
footnoteDefinition: { nodeType: FootnoteDefinition.name },
footnoteReference: { nodeType: FootnoteReference.name },
frontmatter: { nodeType: Frontmatter.name },
@@ -118,8 +133,10 @@ const {
tableCell: { nodeType: TableCell.name },
tableHeader: { nodeType: TableHeader.name },
tableRow: { nodeType: TableRow.name },
+ tableOfContents: { nodeType: TableOfContents.name },
taskItem: { nodeType: TaskItem.name },
taskList: { nodeType: TaskList.name },
+ video: { nodeType: Video.name },
...HTMLNodes.reduce(
(builders, htmlNode) => ({
...builders,
@@ -1233,6 +1250,62 @@ title: 'layout'
),
),
},
+ ...SAFE_AUDIO_EXT.map((extension) => {
+ const src = `http://test.host/video.${extension}`;
+ const markdown = `![audio](${src})`;
+
+ return {
+ markdown,
+ expectedDoc: doc(
+ paragraph(
+ source(markdown),
+ audio({
+ ...source(markdown),
+ canonicalSrc: src,
+ src,
+ alt: 'audio',
+ }),
+ ),
+ ),
+ };
+ }),
+ ...SAFE_VIDEO_EXT.map((extension) => {
+ const src = `http://test.host/video.${extension}`;
+ const markdown = `![video](${src})`;
+
+ return {
+ markdown,
+ expectedDoc: doc(
+ paragraph(
+ source(markdown),
+ video({
+ ...source(markdown),
+ canonicalSrc: src,
+ src,
+ alt: 'video',
+ }),
+ ),
+ ),
+ };
+ }),
+ ...DIAGRAM_LANGUAGES.map((language) => {
+ const markdown = `\`\`\`${language}
+content
+\`\`\``;
+
+ return {
+ markdown,
+ expectedDoc: doc(diagram({ ...source(markdown), language }, 'content')),
+ };
+ }),
+ {
+ markdown: '[[_TOC_]]',
+ expectedDoc: doc(tableOfContents(source('[[_TOC_]]'))),
+ },
+ {
+ markdown: '[TOC]',
+ expectedDoc: doc(tableOfContents(source('[TOC]'))),
+ },
];
const runOnly = examples.find((example) => example.only === true);
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index 4a57c7b1942..bd48b7fdd23 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -1,6 +1,7 @@
import { DOMSerializer } from 'prosemirror-model';
// TODO: DRY up duplication with spec/frontend/content_editor/services/markdown_serializer_spec.js
// See https://gitlab.com/groups/gitlab-org/-/epics/7719#plan
+import Audio from '~/content_editor/extensions/audio';
import Blockquote from '~/content_editor/extensions/blockquote';
import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
@@ -33,13 +34,16 @@ import Table from '~/content_editor/extensions/table';
import TableCell from '~/content_editor/extensions/table_cell';
import TableHeader from '~/content_editor/extensions/table_header';
import TableRow from '~/content_editor/extensions/table_row';
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
+import Video from '~/content_editor/extensions/video';
import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import { createTestEditor } from 'jest/content_editor/test_utils';
const tiptapEditor = createTestEditor({
extensions: [
+ Audio,
Blockquote,
Bold,
BulletList,
@@ -72,8 +76,10 @@ const tiptapEditor = createTestEditor({
TableCell,
TableHeader,
TableRow,
+ TableOfContents,
TaskItem,
TaskList,
+ Video,
],
});
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index a3553e612ca..6175cbdd3d4 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -1,8 +1,3 @@
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
-} from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
@@ -14,6 +9,7 @@ describe('content_editor/services/content_editor', () => {
let eventHub;
let doc;
let p;
+ const testMarkdown = '**bold text**';
beforeEach(() => {
const tiptapEditor = createTestEditor();
@@ -36,6 +32,9 @@ describe('content_editor/services/content_editor', () => {
});
});
+ const testDoc = () => doc(p('document'));
+ const testEmptyDoc = () => doc();
+
describe('.dispose', () => {
it('destroys the tiptapEditor', () => {
expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled();
@@ -46,51 +45,77 @@ describe('content_editor/services/content_editor', () => {
});
});
- describe('when setSerializedContent succeeds', () => {
- let document;
- const languages = ['javascript'];
- const testMarkdown = '**bold text**';
+ describe('empty', () => {
+ it('returns true when tiptapEditor is empty', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testEmptyDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
- beforeEach(() => {
- document = doc(p('document'));
- deserializer.deserialize.mockResolvedValueOnce({ document, languages });
+ expect(contentEditor.empty).toBe(true);
});
- it('emits loadingContent and loadingSuccess event in the eventHub', () => {
- let loadingContentEmitted = false;
+ it('returns false when tiptapEditor is not empty', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() });
- eventHub.$on(LOADING_CONTENT_EVENT, () => {
- loadingContentEmitted = true;
- });
- eventHub.$on(LOADING_SUCCESS_EVENT, () => {
- expect(loadingContentEmitted).toBe(true);
- });
+ await contentEditor.setSerializedContent(testMarkdown);
- contentEditor.setSerializedContent(testMarkdown);
+ expect(contentEditor.empty).toBe(false);
});
+ });
- it('sets the deserialized document in the tiptap editor object', async () => {
- await contentEditor.setSerializedContent(testMarkdown);
+ describe('editable', () => {
+ it('returns true when tiptapEditor is editable', async () => {
+ contentEditor.setEditable(true);
- expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
+ expect(contentEditor.editable).toBe(true);
+ });
+
+ it('returns false when tiptapEditor is readonly', async () => {
+ contentEditor.setEditable(false);
+
+ expect(contentEditor.editable).toBe(false);
});
});
- describe('when setSerializedContent fails', () => {
- const error = 'error';
+ describe('changed', () => {
+ it('returns true when the initial document changes', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ contentEditor.tiptapEditor.commands.insertContent(' new content');
+
+ expect(contentEditor.changed).toBe(true);
+ });
+
+ it('returns false when the initial document hasn’t changed', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ expect(contentEditor.changed).toBe(false);
+ });
+
+ it('returns false when an initial document is not set and the document is empty', () => {
+ expect(contentEditor.changed).toBe(false);
+ });
- beforeEach(() => {
- deserializer.deserialize.mockRejectedValueOnce(error);
+ it('returns true when an initial document is not set and the document is not empty', () => {
+ contentEditor.tiptapEditor.commands.insertContent('new content');
+
+ expect(contentEditor.changed).toBe(true);
});
+ });
+
+ describe('when setSerializedContent succeeds', () => {
+ it('sets the deserialized document in the tiptap editor object', async () => {
+ const document = testDoc();
+
+ deserializer.deserialize.mockResolvedValueOnce({ document });
- it('emits loadingError event', async () => {
- eventHub.$on(LOADING_ERROR_EVENT, (e) => {
- expect(e).toBe('error');
- });
+ await contentEditor.setSerializedContent(testMarkdown);
- await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual(
- error,
- );
+ expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
});
});
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 0e5281be9bf..56394c85e8b 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -1,3 +1,4 @@
+import Audio from '~/content_editor/extensions/audio';
import Blockquote from '~/content_editor/extensions/blockquote';
import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
@@ -33,6 +34,7 @@ import TableHeader from '~/content_editor/extensions/table_header';
import TableRow from '~/content_editor/extensions/table_row';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
+import Video from '~/content_editor/extensions/video';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import { createTestEditor, createDocBuilder } from '../test_utils';
@@ -41,6 +43,7 @@ jest.mock('~/emoji');
const tiptapEditor = createTestEditor({
extensions: [
+ Audio,
Blockquote,
Bold,
BulletList,
@@ -73,6 +76,7 @@ const tiptapEditor = createTestEditor({
TableRow,
TaskItem,
TaskList,
+ Video,
...HTMLMarks,
...HTMLNodes,
],
@@ -80,6 +84,7 @@ const tiptapEditor = createTestEditor({
const {
builders: {
+ audio,
doc,
blockquote,
bold,
@@ -114,6 +119,7 @@ const {
tableRow,
taskItem,
taskList,
+ video,
},
} = createDocBuilder({
tiptapEditor,
@@ -1230,6 +1236,21 @@ paragraph
);
});
+ it('serializes audio and video elements', () => {
+ expect(
+ serialize(
+ paragraph(
+ audio({ alt: 'audio', canonicalSrc: 'audio.mp3' }),
+ ' and ',
+ video({ alt: 'video', canonicalSrc: 'video.mov' }),
+ ),
+ ),
+ ).toBe(
+ `
+![audio](audio.mp3) and ![video](video.mov)`.trimLeft(),
+ );
+ });
+
const defaultEditAction = (initialContent) => {
tiptapEditor.chain().setContent(initialContent.toJSON()).insertContent(' modified').run();
};
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
index f0e9150cada..57e28b396cf 100644
--- a/spec/frontend/crm/form_spec.js
+++ b/spec/frontend/crm/form_spec.js
@@ -298,7 +298,7 @@ describe('Reusable form component', () => {
`(
'should render the correct component for #$id with the value "$value"',
({ index, id, component, value }) => {
- const findFormElement = () => findFormGroup(index).find(component);
+ const findFormElement = () => findFormGroup(index).findComponent(component);
expect(findFormElement().attributes('id')).toBe(id);
expect(findFormElement().attributes('value')).toBe(value);
@@ -307,7 +307,8 @@ describe('Reusable form component', () => {
it('should render a checked GlFormCheckbox for #active', () => {
const activeCheckboxIndex = 6;
- const findFormElement = () => findFormGroup(activeCheckboxIndex).find(GlFormCheckbox);
+ const findFormElement = () =>
+ findFormGroup(activeCheckboxIndex).findComponent(GlFormCheckbox);
expect(findFormElement().attributes('id')).toBe('active');
expect(findFormElement().attributes('checked')).toBe('true');
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
index a2e2e88ac60..a19ee01c2a5 100644
--- a/spec/frontend/crm/mock_data.js
+++ b/spec/frontend/crm/mock_data.js
@@ -102,6 +102,13 @@ export const getGroupOrganizationsQueryResponse = {
active: true,
},
],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ endCursor: 'eyJsYXN0X25hbWUiOiJMZWRuZXIiLCJpZCI6IjE3OSJ9',
+ hasPreviousPage: false,
+ startCursor: 'eyJsYXN0X25hbWUiOiJCYXJ0b24iLCJpZCI6IjE5MyJ9',
+ },
},
},
},
@@ -155,6 +162,21 @@ export const updateContactMutationResponse = {
},
};
+export const getGroupOrganizationsCountQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/26',
+ organizationStateCounts: {
+ all: 24,
+ active: 21,
+ inactive: 3,
+ __typename: 'OrganizationStateCountsType',
+ },
+ },
+ },
+};
+
export const updateContactMutationErrorResponse = {
data: {
customerRelationsContactUpdate: {
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index 1780a5945a6..a0b56596177 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -1,14 +1,19 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import OrganizationsRoot from '~/crm/organizations/components/organizations_root.vue';
import routes from '~/crm/organizations/routes';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
-import { getGroupOrganizationsQueryResponse } from './mock_data';
+import getGroupOrganizationsCountByStateQuery from '~/crm/organizations/components/graphql/get_group_organizations_count_by_state.query.graphql';
+import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
+import {
+ getGroupOrganizationsQueryResponse,
+ getGroupOrganizationsCountQueryResponse,
+} from './mock_data';
describe('Customer relations organizations root app', () => {
Vue.use(VueApollo);
@@ -21,23 +26,31 @@ describe('Customer relations organizations root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewOrganizationButton = () => wrapper.findByTestId('new-organization-button');
- const findError = () => wrapper.findComponent(GlAlert);
+ const findTable = () => wrapper.findComponent(PaginatedTableWithSearchAndTabs);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse);
+ const successCountQueryHandler = jest
+ .fn()
+ .mockResolvedValue(getGroupOrganizationsCountQueryResponse);
const basePath = '/groups/flightjs/-/crm/organizations';
const mountComponent = ({
queryHandler = successQueryHandler,
- mountFunction = shallowMountExtended,
+ countQueryHandler = successCountQueryHandler,
canAdminCrmOrganization = true,
+ textQuery = null,
} = {}) => {
- fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]);
- wrapper = mountFunction(OrganizationsRoot, {
+ fakeApollo = createMockApollo([
+ [getGroupOrganizationsQuery, queryHandler],
+ [getGroupOrganizationsCountByStateQuery, countQueryHandler],
+ ]);
+ wrapper = mountExtended(OrganizationsRoot, {
router,
provide: {
canAdminCrmOrganization,
groupFullPath: 'flightjs',
groupIssuesPath: '/issues',
+ textQuery,
},
apolloProvider: fakeApollo,
});
@@ -57,9 +70,33 @@ describe('Customer relations organizations root app', () => {
router = null;
});
- it('should render loading spinner', () => {
+ it('should render table with default props and loading spinner', () => {
mountComponent();
+ expect(findTable().props()).toMatchObject({
+ items: [],
+ itemsCount: {},
+ pageInfo: {},
+ statusTabs: [
+ { title: 'Active', status: 'ACTIVE', filters: 'active' },
+ { title: 'Inactive', status: 'INACTIVE', filters: 'inactive' },
+ { title: 'All', status: 'ALL', filters: 'all' },
+ ],
+ showItems: true,
+ showErrorMsg: false,
+ trackViewsOptions: { category: 'Customer Relations', action: 'view_organizations_list' },
+ i18n: {
+ emptyText: 'No organizations found',
+ issuesButtonLabel: 'View issues',
+ editButtonLabel: 'Edit',
+ title: 'Customer relations organizations',
+ newOrganization: 'New organization',
+ errorText: 'Something went wrong. Please try again.',
+ },
+ serverErrorMessage: '',
+ filterSearchKey: 'organizations',
+ filterSearchTokens: [],
+ });
expect(findLoadingIcon().exists()).toBe(true);
});
@@ -77,11 +114,25 @@ describe('Customer relations organizations root app', () => {
});
});
- it('should render error message on reject', async () => {
- mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
- await waitForPromises();
+ describe('error', () => {
+ it('should render on reject', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Something went wrong. Please try again.');
+ });
+
+ it('should be removed on error-alert-dismissed event', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
- expect(findError().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Something went wrong. Please try again.');
+
+ findTable().vm.$emit('error-alert-dismissed');
+ await waitForPromises();
+
+ expect(wrapper.text()).not.toContain('Something went wrong. Please try again.');
+ });
});
describe('on successful load', () => {
@@ -89,20 +140,27 @@ describe('Customer relations organizations root app', () => {
mountComponent();
await waitForPromises();
- expect(findError().exists()).toBe(false);
+ expect(wrapper.text()).not.toContain('Something went wrong. Please try again.');
});
it('renders correct results', async () => {
- mountComponent({ mountFunction: mountExtended });
+ mountComponent();
await waitForPromises();
expect(findRowByName(/Test Inc/i)).toHaveLength(1);
expect(findRowByName(/VIP/i)).toHaveLength(1);
expect(findRowByName(/120/i)).toHaveLength(1);
- const issueLink = findIssuesLinks().at(0);
- expect(issueLink.exists()).toBe(true);
- expect(issueLink.attributes('href')).toBe('/issues?crm_organization_id=2');
+ expect(findIssuesLinks()).toHaveLength(3);
+
+ const links = findIssuesLinks().wrappers.map((w) => w.attributes('href'));
+ expect(links).toEqual(
+ expect.arrayContaining([
+ '/issues?crm_organization_id=1',
+ '/issues?crm_organization_id=2',
+ '/issues?crm_organization_id=3',
+ ]),
+ );
});
});
});
diff --git a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap b/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap
index 7f211c1028e..92927ef16ec 100644
--- a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap
+++ b/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap
@@ -1,28 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`TotalTime with a blank object should render -- 1`] = `"<span class=\\"total-time\\"> -- </span>"`;
+exports[`TotalTime with a blank object should render -- 1`] = `"<span> -- </span>"`;
exports[`TotalTime with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = `
-"<span class=\\"total-time\\">
+"<span>
3 <span>days</span></span>"
`;
exports[`TotalTime with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = `
-"<span class=\\"total-time\\">
+"<span>
7 <span>hrs</span></span>"
`;
exports[`TotalTime with a valid time object with {"hours": 23, "mins": 10} 1`] = `
-"<span class=\\"total-time\\">
+"<span>
23 <span>hrs</span></span>"
`;
exports[`TotalTime with a valid time object with {"mins": 47, "seconds": 3} 1`] = `
-"<span class=\\"total-time\\">
+"<span>
47 <span>mins</span></span>"
`;
exports[`TotalTime with a valid time object with {"seconds": 35} 1`] = `
-"<span class=\\"total-time\\">
+"<span>
35 <span>s</span></span>"
`;
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index ea3da86c7b2..013bea671a8 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -201,7 +201,7 @@ describe('Value stream analytics component', () => {
it('renders the stage table with a loading icon', () => {
const tableWrapper = findStageTable();
expect(tableWrapper.exists()).toBe(true);
- expect(tableWrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(tableWrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders the path navigation loading state', () => {
diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/cycle_analytics/path_navigation_spec.js
index fa9eadbd071..fec1526359c 100644
--- a/spec/frontend/cycle_analytics/path_navigation_spec.js
+++ b/spec/frontend/cycle_analytics/path_navigation_spec.js
@@ -56,7 +56,9 @@ describe('Project PathNavigation', () => {
describe('displays correctly', () => {
it('has the correct props', () => {
- expect(wrapper.find(GlPath).props('items')).toMatchObject(transformedProjectStagePathData);
+ expect(wrapper.findComponent(GlPath).props('items')).toMatchObject(
+ transformedProjectStagePathData,
+ );
});
it('contains all the expected stages', () => {
@@ -69,11 +71,11 @@ describe('Project PathNavigation', () => {
describe('loading', () => {
describe('is false', () => {
it('displays the gl-path component', () => {
- expect(wrapper.find(GlPath).exists()).toBe(true);
+ expect(wrapper.findComponent(GlPath).exists()).toBe(true);
});
it('hides the gl-skeleton-loading component', () => {
- expect(wrapper.find(GlSkeletonLoader).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
});
it('renders each stage', () => {
@@ -112,11 +114,11 @@ describe('Project PathNavigation', () => {
});
it('hides the gl-path component', () => {
- expect(wrapper.find(GlPath).exists()).toBe(false);
+ expect(wrapper.findComponent(GlPath).exists()).toBe(false);
});
it('displays the gl-skeleton-loading component', () => {
- expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index 23e41f35b00..9c8cd6a3dbc 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -176,7 +176,7 @@ describe('ValueStreamMetrics', () => {
await waitForPromises();
});
- it('it should render an error message', () => {
+ it('should render an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
message: `There was an error while fetching value stream analytics ${fakeReqName} data.`,
});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
index 7c46c280d46..bbafdc000db 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
@@ -42,7 +42,7 @@ describe('Deploy freeze modal', () => {
wrapper.find('#deploy-freeze-start').trigger('input');
wrapper.find('#deploy-freeze-end').trigger('input');
- wrapper.find(TimezoneDropdown).trigger('input');
+ wrapper.findComponent(TimezoneDropdown).trigger('input');
};
afterEach(() => {
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
index cc044800e5e..637efe30022 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -31,11 +31,11 @@ describe('Deploy freeze settings', () => {
describe('Deploy freeze table contains components', () => {
it('contains deploy freeze table', () => {
- expect(wrapper.find(DeployFreezeTable).exists()).toBe(true);
+ expect(wrapper.findComponent(DeployFreezeTable).exists()).toBe(true);
});
it('contains deploy freeze modal', () => {
- expect(wrapper.find(DeployFreezeModal).exists()).toBe(true);
+ expect(wrapper.findComponent(DeployFreezeModal).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
index aea81daecef..567d18f8b92 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -30,8 +30,8 @@ describe('Deploy freeze timezone dropdown', () => {
wrapper.setData({ searchTerm });
};
- const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
- const findDropdownItemByIndex = (index) => wrapper.findAll(GlDropdownItem).at(index);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
afterEach(() => {
wrapper.destroy();
@@ -96,7 +96,7 @@ describe('Deploy freeze timezone dropdown', () => {
});
it('renders selected time zone as dropdown label', () => {
- expect(wrapper.find(GlDropdown).vm.text).toBe('Alaska');
+ expect(wrapper.findComponent(GlDropdown).vm.text).toBe('Alaska');
});
});
});
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index b18d53b317d..4a070395eaf 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -314,7 +314,7 @@ describe('deprecatedJQueryDropdown', () => {
});
describe('with a trackSuggestionsClickedLabel', () => {
- it('it includes data-track attributes', () => {
+ it('includes data-track attributes', () => {
const dropdown = dropdownWithOptions({
trackSuggestionClickedLabel: 'some_value_for_label',
});
@@ -333,7 +333,7 @@ describe('deprecatedJQueryDropdown', () => {
expect(link).toHaveAttr('data-track-property', 'suggestion-category');
});
- it('it defaults property to no_category when category not provided', () => {
+ it('defaults property to no_category when category not provided', () => {
const dropdown = dropdownWithOptions({
trackSuggestionClickedLabel: 'some_value_for_label',
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 28833b4af5c..df511586c10 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -43,6 +43,7 @@ describe('Design note component', () => {
wrapper = shallowMountExtended(DesignNote, {
propsData: {
note: {},
+ noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
data() {
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index f7ce742b933..e36f5c79e3e 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import Autosave from '~/autosave';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
const showModal = jest.fn();
@@ -13,6 +14,7 @@ const GlModal = {
describe('Design reply form component', () => {
let wrapper;
+ let originalGon;
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
@@ -24,6 +26,7 @@ describe('Design reply form component', () => {
propsData: {
value: '',
isSaving: false,
+ noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
stubs: { GlModal },
@@ -31,8 +34,14 @@ describe('Design reply form component', () => {
});
}
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon.current_user_id = 1;
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
it('textarea has focus after component mount', () => {
@@ -66,6 +75,25 @@ describe('Design reply form component', () => {
expect(findSubmitButton().html()).toMatchSnapshot();
});
+ it.each`
+ discussionId | shortDiscussionId
+ ${undefined} | ${'new'}
+ ${'gid://gitlab/DiffDiscussion/123'} | ${123}
+ `(
+ 'initializes autosave support on discussion with proper key',
+ async ({ discussionId, shortDiscussionId }) => {
+ createComponent({ discussionId });
+ await nextTick();
+
+ // We discourage testing `wrapper.vm` properties but
+ // since `autosave` library instantiates on component
+ // there's no other way to test whether instantiation
+ // happened correctly or not.
+ expect(wrapper.vm.autosaveDiscussion).toBeInstanceOf(Autosave);
+ expect(wrapper.vm.autosaveDiscussion.key).toBe(`autosave/Discussion/6/${shortDiscussionId}`);
+ },
+ );
+
describe('when form has no text', () => {
beforeEach(() => {
createComponent({
@@ -120,28 +148,37 @@ describe('Design reply form component', () => {
});
it('emits submitForm event on Comment button click', async () => {
+ const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+
findSubmitButton().vm.$emit('click');
await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
it('emits submitForm event on textarea ctrl+enter keydown', async () => {
+ const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+
findTextarea().trigger('keydown.enter', {
ctrlKey: true,
});
await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
it('emits submitForm event on textarea meta+enter keydown', async () => {
+ const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+
findTextarea().trigger('keydown.enter', {
metaKey: true,
});
await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
it('emits input event on changing textarea content', async () => {
@@ -180,10 +217,13 @@ describe('Design reply form component', () => {
});
it('emits cancelForm event on modal Ok button click', () => {
+ const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+
findTextarea().trigger('keyup.esc');
findModal().vm.$emit('ok');
expect(wrapper.emitted('cancel-form')).toBeTruthy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index 30eddcee86a..4a339899473 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -525,7 +525,7 @@ describe('Design management design presentation component', () => {
{ clientX: 10, clientY: 10 },
{ mouseup: true },
).then(() => {
- expect(wrapper.emitted('openCommentForm')).toBeFalsy();
+ expect(wrapper.emitted('openCommentForm')).toBeUndefined();
});
});
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 8fe3e92360a..096d776a7d2 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -11,7 +11,7 @@ exports[`Design management list item component when item appears in view after i
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
<router-link-stub
ariacurrentvalue="page"
- class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new gl-mb-0"
event="click"
tag="a"
to="[object Object]"
@@ -88,7 +88,7 @@ exports[`Design management list item component with notes renders item with mult
exports[`Design management list item component with notes renders item with single comment 1`] = `
<router-link-stub
ariacurrentvalue="page"
- class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new gl-mb-0"
event="click"
tag="a"
to="[object Object]"
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index b6137ba2eee..1776405ece9 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -107,7 +107,7 @@ describe('Design management toolbar component', () => {
await nextTick();
wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs');
- expect(wrapper.emitted().delete).toBeTruthy();
+ expect(wrapper.emitted().delete).toHaveLength(1);
});
it('renders download button with correct link', () => {
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index 9997f02cd01..8cfe11c9040 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -9,9 +9,7 @@ exports[`Design management index page designs renders error 1`] = `
<!---->
- <div
- class="gl-mt-6"
- >
+ <div>
<gl-alert-stub
dismisslabel="Dismiss"
primarybuttonlink=""
@@ -43,9 +41,7 @@ exports[`Design management index page designs renders loading icon 1`] = `
<!---->
- <div
- class="gl-mt-6"
- >
+ <div>
<gl-loading-icon-stub
color="dark"
label="Loading"
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 3177a5e016c..d86fbf81d20 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -64,7 +64,7 @@ exports[`Design management design index page renders design index 1`] = `
<participants-stub
class="gl-mb-4"
lazy="true"
- numberoflessparticipants="7"
+ numberoflessparticipants="8"
participants="[object Object]"
/>
@@ -195,7 +195,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<participants-stub
class="gl-mb-4"
lazy="true"
- numberoflessparticipants="7"
+ numberoflessparticipants="8"
participants="[object Object]"
/>
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index f90feaadfb0..1033b509419 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -254,7 +254,7 @@ describe('Design management index page', () => {
'gl-flex-direction-column',
'col-md-6',
'col-lg-3',
- 'gl-mb-3',
+ 'gl-mt-5',
]);
});
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 96f2ac1692c..b88206c3b9a 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -30,7 +30,7 @@ const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`;
Vue.use(Vuex);
function getCollapsedFilesWarning(wrapper) {
- return wrapper.find(CollapsedFilesWarning);
+ return wrapper.findComponent(CollapsedFilesWarning);
}
describe('diffs/components/app', () => {
@@ -167,7 +167,7 @@ describe('diffs/components/app', () => {
state.diffs.isLoading = true;
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('displays loading icon on batch loading', () => {
@@ -175,13 +175,13 @@ describe('diffs/components/app', () => {
state.diffs.batchLoadingState = 'loading';
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('displays diffs container when not loading', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('#diffs').exists()).toBe(true);
});
@@ -263,7 +263,7 @@ describe('diffs/components/app', () => {
it('renders empty state when no diff files exist', () => {
createComponent();
- expect(wrapper.find(NoChanges).exists()).toBe(true);
+ expect(wrapper.findComponent(NoChanges).exists()).toBe(true);
});
it('does not render empty state when diff files exist', () => {
@@ -273,8 +273,8 @@ describe('diffs/components/app', () => {
});
});
- expect(wrapper.find(NoChanges).exists()).toBe(false);
- expect(wrapper.findAll(DiffFile).length).toBe(1);
+ expect(wrapper.findComponent(NoChanges).exists()).toBe(false);
+ expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
});
});
@@ -487,8 +487,8 @@ describe('diffs/components/app', () => {
state.diffs.mergeRequestDiff = mergeRequestDiff;
});
- expect(wrapper.find(CompareVersions).exists()).toBe(true);
- expect(wrapper.find(CompareVersions).props()).toEqual(
+ expect(wrapper.findComponent(CompareVersions).exists()).toBe(true);
+ expect(wrapper.findComponent(CompareVersions).props()).toEqual(
expect.objectContaining({
diffFilesCountText: null,
}),
@@ -506,8 +506,8 @@ describe('diffs/components/app', () => {
state.diffs.size = 1;
});
- expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true);
- expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
+ expect(wrapper.findComponent(HiddenFilesWarning).exists()).toBe(true);
+ expect(wrapper.findComponent(HiddenFilesWarning).props()).toEqual(
expect.objectContaining({
total: '5',
plainDiffPath: 'plain diff path',
@@ -547,7 +547,7 @@ describe('diffs/components/app', () => {
};
});
- expect(wrapper.find(CommitWidget).exists()).toBe(true);
+ expect(wrapper.findComponent(CommitWidget).exists()).toBe(true);
});
it('should display diff file if there are diff files', () => {
@@ -555,13 +555,13 @@ describe('diffs/components/app', () => {
state.diffs.diffFiles.push({ sha: '123' });
});
- expect(wrapper.find(DiffFile).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffFile).exists()).toBe(true);
});
it("doesn't render tree list when no changes exist", () => {
createComponent();
- expect(wrapper.find(TreeList).exists()).toBe(false);
+ expect(wrapper.findComponent(TreeList).exists()).toBe(false);
});
it('should render tree list', () => {
@@ -569,7 +569,7 @@ describe('diffs/components/app', () => {
state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
});
- expect(wrapper.find(TreeList).exists()).toBe(true);
+ expect(wrapper.findComponent(TreeList).exists()).toBe(true);
});
});
@@ -636,12 +636,12 @@ describe('diffs/components/app', () => {
await nextTick();
- expect(wrapper.findAll(DiffFile).length).toBe(1);
+ expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
});
describe('pagination', () => {
const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
- const paginator = () => fileByFileNav().find(GlPagination);
+ const paginator = () => fileByFileNav().findComponent(GlPagination);
it('sets previous button as disabled', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
@@ -682,7 +682,7 @@ describe('diffs/components/app', () => {
${'123'} | ${2}
${'312'} | ${1}
`(
- 'it calls navigateToDiffFileIndex with $index when $link is clicked',
+ 'calls navigateToDiffFileIndex with $index when $link is clicked',
async ({ currentDiffFileId, targetFile }) => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
index cc4f13ab0cf..eca5b536a35 100644
--- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -28,8 +28,8 @@ describe('CollapsedFilesWarning', () => {
Vue.use(Vuex);
const getAlertActionButton = () =>
- wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child');
- const getAlertCloseButton = () => wrapper.find(CollapsedFilesWarning).find('button');
+ wrapper.findComponent(CollapsedFilesWarning).find('button.gl-alert-action:first-child');
+ const getAlertCloseButton = () => wrapper.findComponent(CollapsedFilesWarning).find('button');
const createComponent = (props = {}, { full } = { full: false }) => {
const mounter = full ? mount : shallowMount;
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index e52c5abbc7b..440f169be86 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -27,7 +27,7 @@ describe('diffs/components/commit_item', () => {
const getAvatarElement = () => wrapper.find('.user-avatar-link');
const getCommitterElement = () => wrapper.find('.committer');
const getCommitActionsElement = () => wrapper.find('.commit-actions');
- const getCommitPipelineStatus = () => wrapper.find(CommitPipelineStatus);
+ const getCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
const mountComponent = (propsData) => {
wrapper = mount(Component, {
@@ -111,8 +111,8 @@ describe('diffs/components/commit_item', () => {
const descElement = getDescElement();
const descExpandElement = getDescExpandElement();
- expect(descElement.exists()).toBeFalsy();
- expect(descExpandElement.exists()).toBeFalsy();
+ expect(descElement.exists()).toBe(false);
+ expect(descExpandElement.exists()).toBe(false);
});
});
diff --git a/spec/frontend/diffs/components/commit_widget_spec.js b/spec/frontend/diffs/components/commit_widget_spec.js
index fbff473e4df..f650ead6f83 100644
--- a/spec/frontend/diffs/components/commit_widget_spec.js
+++ b/spec/frontend/diffs/components/commit_widget_spec.js
@@ -12,7 +12,7 @@ describe('diffs/components/commit_widget', () => {
});
it('renders commit item', () => {
- const commitElement = wrapper.find(CommitItem);
+ const commitElement = wrapper.findComponent(CommitItem);
expect(commitElement.exists()).toBe(true);
});
diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
index 98f88226742..09128b04caa 100644
--- a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
+++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
@@ -34,7 +34,7 @@ describe('CompareDropdownLayout', () => {
findListItems().wrappers.map((listItem) => ({
href: listItem.find('a').attributes('href'),
text: trimText(listItem.text()),
- createdAt: listItem.findAll(TimeAgo).wrappers[0]?.props('time'),
+ createdAt: listItem.findAllComponents(TimeAgo).wrappers[0]?.props('time'),
isActive: listItem.classes().includes('is-active'),
}));
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index 81a817c47dc..b5dce4fc924 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -17,7 +17,6 @@ describe('DiffCodeQuality', () => {
return mountFunction(DiffCodeQuality, {
propsData: {
expandedLines: [],
- line: 1,
codeQuality,
},
});
@@ -28,9 +27,7 @@ describe('DiffCodeQuality', () => {
expect(wrapper.findByTestId('diff-codequality').exists()).toBe(true);
await wrapper.findByTestId('diff-codequality-close').trigger('click');
-
expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1);
- expect(wrapper.emitted().hideCodeQualityFindings[0][0]).toBe(wrapper.props('line'));
});
it('renders correct amount of list items for codequality array and their description', async () => {
diff --git a/spec/frontend/diffs/components/diff_comment_cell_spec.js b/spec/frontend/diffs/components/diff_comment_cell_spec.js
index b636a178593..2acfc2c6d7e 100644
--- a/spec/frontend/diffs/components/diff_comment_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_comment_cell_spec.js
@@ -20,24 +20,24 @@ describe('DiffCommentCell', () => {
it('renders discussions if line has discussions', () => {
const wrapper = createWrapper({ renderDiscussion: true });
- expect(wrapper.find(DiffDiscussions).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffDiscussions).exists()).toBe(true);
});
it('does not render discussions if line has no discussions', () => {
const wrapper = createWrapper();
- expect(wrapper.find(DiffDiscussions).exists()).toBe(false);
+ expect(wrapper.findComponent(DiffDiscussions).exists()).toBe(false);
});
it('renders discussion reply if line has no draft', () => {
const wrapper = createWrapper();
- expect(wrapper.find(DiffDiscussionReply).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffDiscussionReply).exists()).toBe(true);
});
it('does not render discussion reply if line has draft', () => {
const wrapper = createWrapper({ hasDraft: true });
- expect(wrapper.find(DiffDiscussionReply).exists()).toBe(false);
+ expect(wrapper.findComponent(DiffDiscussionReply).exists()).toBe(false);
});
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 6844e6e497a..9f593ee0d49 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -110,13 +110,13 @@ describe('DiffContent', () => {
props: { diffFile: textDiffFile },
});
- expect(wrapper.find(DiffView).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffView).exists()).toBe(true);
});
it('renders rendering more lines loading icon', () => {
createComponent({ props: { diffFile: { ...textDiffFile, renderingLines: true } } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -133,7 +133,7 @@ describe('DiffContent', () => {
props: { diffFile: { ...emptyDiffFile, viewer: { name: diffViewerModes.no_preview } } },
});
- expect(wrapper.find(NoPreviewViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(NoPreviewViewer).exists()).toBe(true);
});
it('should render not diffable view if viewer set to non_diffable', () => {
@@ -141,7 +141,7 @@ describe('DiffContent', () => {
props: { diffFile: { ...emptyDiffFile, viewer: { name: diffViewerModes.not_diffable } } },
});
- expect(wrapper.find(NotDiffableViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(NotDiffableViewer).exists()).toBe(true);
});
});
@@ -156,7 +156,7 @@ describe('DiffContent', () => {
},
});
- expect(wrapper.find(DiffDiscussions).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffDiscussions).exists()).toBe(true);
});
it('emits saveDiffDiscussion when note-form emits `handleFormUpdate`', () => {
@@ -169,7 +169,7 @@ describe('DiffContent', () => {
},
});
- wrapper.find(NoteForm).vm.$emit('handleFormUpdate', noteStub);
+ wrapper.findComponent(NoteForm).vm.$emit('handleFormUpdate', noteStub);
expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), {
note: noteStub,
formData: {
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
index f03c0357a0e..5ccd2002462 100644
--- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -64,7 +64,7 @@ describe('DiffDiscussionReply', () => {
hasForm: false,
});
- expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
});
});
@@ -83,6 +83,6 @@ describe('DiffDiscussionReply', () => {
hasForm: false,
});
- expect(wrapper.find(NoteSignedOutWidget).exists()).toBe(true);
+ expect(wrapper.findComponent(NoteSignedOutWidget).exists()).toBe(true);
});
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 2da68adddf6..e9a0e0745fd 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -32,11 +32,11 @@ describe('DiffDiscussions', () => {
it('should have notes list', () => {
createComponent();
- expect(wrapper.find(NoteableDiscussion).exists()).toBe(true);
- expect(wrapper.find(DiscussionNotes).exists()).toBe(true);
- expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe(
- discussionsMockData.notes.length,
- );
+ expect(wrapper.findComponent(NoteableDiscussion).exists()).toBe(true);
+ expect(wrapper.findComponent(DiscussionNotes).exists()).toBe(true);
+ expect(
+ wrapper.findComponent(DiscussionNotes).findAllComponents(TimelineEntryItem).length,
+ ).toBe(discussionsMockData.notes.length);
});
});
@@ -48,7 +48,7 @@ describe('DiffDiscussions', () => {
const diffNotesToggle = findDiffNotesToggle();
expect(diffNotesToggle.exists()).toBe(true);
- expect(diffNotesToggle.find(GlIcon).exists()).toBe(true);
+ expect(diffNotesToggle.findComponent(GlIcon).exists()).toBe(true);
expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true);
});
@@ -80,12 +80,12 @@ describe('DiffDiscussions', () => {
discussions[0].expanded = false;
createComponent({ discussions, shouldCollapseDiscussions: true });
- expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false);
+ expect(wrapper.findComponent(NoteableDiscussion).isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
createComponent({ renderAvatarBadge: true });
- const noteableDiscussion = wrapper.find(NoteableDiscussion);
+ const noteableDiscussion = wrapper.findComponent(NoteableDiscussion);
expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true);
expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1');
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index 92b8b2d4aa3..c23eb2f3d24 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -76,18 +76,19 @@ describe('DiffFileHeader component', () => {
wrapper.destroy();
});
- const findHeader = () => wrapper.find({ ref: 'header' });
- const findTitleLink = () => wrapper.find({ ref: 'titleWrapper' });
- const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' });
+ const findHeader = () => wrapper.findComponent({ ref: 'header' });
+ const findTitleLink = () => wrapper.findComponent({ ref: 'titleWrapper' });
+ const findExpandButton = () => wrapper.findComponent({ ref: 'expandDiffToFullFileButton' });
const findFileActions = () => wrapper.find('.file-actions');
- const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' });
+ const findModeChangedLine = () => wrapper.findComponent({ ref: 'fileMode' });
const findLfsLabel = () => wrapper.find('[data-testid="label-lfs"]');
- const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' });
- const findExternalLink = () => wrapper.find({ ref: 'externalLink' });
- const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' });
- const findViewFileButton = () => wrapper.find({ ref: 'viewButton' });
- const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
- const findEditButton = () => wrapper.find({ ref: 'editButton' });
+ const findToggleDiscussionsButton = () =>
+ wrapper.findComponent({ ref: 'toggleDiscussionsButton' });
+ const findExternalLink = () => wrapper.findComponent({ ref: 'externalLink' });
+ const findReplacedFileButton = () => wrapper.findComponent({ ref: 'replacedFileButton' });
+ const findViewFileButton = () => wrapper.findComponent({ ref: 'viewButton' });
+ const findCollapseIcon = () => wrapper.findComponent({ ref: 'collapseIcon' });
+ const findEditButton = () => wrapper.findComponent({ ref: 'editButton' });
const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
const createComponent = ({ props, options = {} } = {}) => {
@@ -153,7 +154,7 @@ describe('DiffFileHeader component', () => {
});
it('displays a copy to clipboard button', () => {
- expect(wrapper.find(ClipboardButton).exists()).toBe(true);
+ expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true);
});
it('triggers the copy to clipboard tracking event', () => {
diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js
index 1d1c5fec293..c5b76551fcc 100644
--- a/spec/frontend/diffs/components/diff_file_row_spec.js
+++ b/spec/frontend/diffs/components/diff_file_row_spec.js
@@ -32,7 +32,7 @@ describe('Diff File Row component', () => {
...diffFileRowProps,
});
- expect(wrapper.find(FileRow).props()).toEqual(
+ expect(wrapper.findComponent(FileRow).props()).toEqual(
expect.objectContaining({
...sharedProps,
}),
@@ -47,7 +47,7 @@ describe('Diff File Row component', () => {
showTooltip: true,
});
- expect(wrapper.find(ChangedFileIcon).props()).toEqual(
+ expect(wrapper.findComponent(ChangedFileIcon).props()).toEqual(
expect.objectContaining({
file: {},
size: 16,
@@ -74,7 +74,7 @@ describe('Diff File Row component', () => {
hideFileStats: false,
viewedFiles: isViewed ? { '#123456789': true } : {},
});
- expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected);
+ expect(wrapper.findComponent(FileRow).props('fileClasses')).toBe(expected);
},
);
@@ -92,7 +92,7 @@ describe('Diff File Row component', () => {
},
hideFileStats,
});
- expect(wrapper.find(FileRowStats).exists()).toEqual(value);
+ expect(wrapper.findComponent(FileRowStats).exists()).toEqual(value);
});
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 9e8d9e1ca29..944cec77efb 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -100,7 +100,7 @@ function createComponent({ file, first = false, last = false, options = {}, prop
};
}
-const findDiffHeader = (wrapper) => wrapper.find(DiffFileHeaderComponent);
+const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent);
const findDiffContentArea = (wrapper) => wrapper.find('[data-testid="content-area"]');
const findLoader = (wrapper) => wrapper.find('[data-testid="loader-icon"]');
const findToggleButton = (wrapper) => wrapper.find('[data-testid="expand-button"]');
@@ -209,14 +209,14 @@ describe('DiffFile', () => {
expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
expect(el.querySelector('.js-file-title')).toBeDefined();
- expect(wrapper.find(DiffFileHeaderComponent).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffFileHeaderComponent).exists()).toBe(true);
expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
markFileToBeRendered(store);
await nextTick();
- expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true);
});
});
@@ -320,7 +320,7 @@ describe('DiffFile', () => {
});
it('should have the file content', async () => {
- expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true);
});
it('should style the component so that it `.has-body` for layout purposes', () => {
@@ -473,8 +473,8 @@ describe('DiffFile', () => {
await nextTick();
expect(wrapper.classes('has-body')).toBe(true);
- expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
- expect(wrapper.find(DiffContentComponent).isVisible()).toBe(true);
+ expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffContentComponent).isVisible()).toBe(true);
},
);
});
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
index c18f0b721da..f13988fc11f 100644
--- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import { HIDE_COMMENTS } from '~/diffs/i18n';
import discussionsMockData from '../mock_data/diff_discussions';
const getDiscussionsMockData = () => [{ ...discussionsMockData }];
@@ -40,7 +41,12 @@ describe('DiffGutterAvatars', () => {
findCollapseButton().trigger('click');
await nextTick();
- expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ expect(wrapper.emitted().toggleLineDiscussions).toBeDefined();
+ });
+
+ it('renders the proper title and aria-label', () => {
+ expect(findCollapseButton().attributes('title')).toBe(HIDE_COMMENTS);
+ expect(findCollapseButton().attributes('aria-label')).toBe(HIDE_COMMENTS);
});
});
@@ -69,14 +75,14 @@ describe('DiffGutterAvatars', () => {
findUserAvatars().at(0).trigger('click');
await nextTick();
- expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ expect(wrapper.emitted().toggleLineDiscussions).toBeDefined();
});
it('should emit toggleDiscussions event on more count text click', async () => {
findMoreCount().trigger('click');
await nextTick();
- expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ expect(wrapper.emitted().toggleLineDiscussions).toBeDefined();
});
});
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index 542d61c4680..9493dc8855e 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -82,7 +82,7 @@ describe('DiffLineNoteForm', () => {
});
it('shows note form', () => {
- expect(wrapper.find(NoteForm).exists()).toBe(true);
+ expect(wrapper.findComponent(NoteForm).exists()).toBe(true);
});
it('passes the provided range of lines to comment form', () => {
diff --git a/spec/frontend/diffs/components/diff_line_spec.js b/spec/frontend/diffs/components/diff_line_spec.js
new file mode 100644
index 00000000000..37368eb1461
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_line_spec.js
@@ -0,0 +1,65 @@
+import { shallowMount } from '@vue/test-utils';
+import DiffLine from '~/diffs/components/diff_line.vue';
+import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
+
+const EXAMPLE_LINE_NUMBER = 3;
+const EXAMPLE_DESCRIPTION = 'example description';
+const EXAMPLE_SEVERITY = 'example severity';
+
+const left = {
+ line: {
+ left: {
+ codequality: [
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ],
+ },
+ },
+};
+
+const right = {
+ line: {
+ right: {
+ codequality: [
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ],
+ },
+ },
+};
+
+const mockData = [right, left];
+
+describe('DiffLine', () => {
+ const createWrapper = (propsData) => {
+ return shallowMount(DiffLine, { propsData });
+ };
+
+ it('should emit event when hideCodeQualityFindings is called', () => {
+ const wrapper = createWrapper(right);
+
+ wrapper.findComponent(DiffCodeQuality).vm.$emit('hideCodeQualityFindings');
+ expect(wrapper.emitted()).toEqual({
+ hideCodeQualityFindings: [[EXAMPLE_LINE_NUMBER]],
+ });
+ });
+
+ mockData.forEach((element) => {
+ it('should set correct props for DiffCodeQuality', () => {
+ const wrapper = createWrapper(element);
+ expect(wrapper.findComponent(DiffCodeQuality).props('codeQuality')).toEqual([
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 09fe69e97de..3a04547fa69 100644
--- a/spec/frontend/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
@@ -87,7 +87,7 @@ describe('diff_stats', () => {
describe('files changes', () => {
const findIcon = (name) =>
wrapper
- .findAll(GlIcon)
+ .findAllComponents(GlIcon)
.filter((c) => c.attributes('name') === name)
.at(0).element.parentNode;
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 15923a1c6de..1dd4a2f6c23 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DiffView from '~/diffs/components/diff_view.vue';
-import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
+import DiffLine from '~/diffs/components/diff_line.vue';
import { diffCodeQuality } from '../mock_data/diff_code_quality';
describe('DiffView', () => {
@@ -51,28 +51,27 @@ describe('DiffView', () => {
return shallowMount(DiffView, { propsData, store, stubs, provide });
};
- it('does not render a codeQuality diff view when there is no finding', () => {
+ it('does not render a diff-line component when there is no finding', () => {
const wrapper = createWrapper();
- expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(false);
+ expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
});
- it('does render a codeQuality diff view with the correct props when there is a finding & refactorCodeQualityInlineFindings flag is true ', async () => {
+ it('does render a diff-line component with the correct props when there is a finding & refactorCodeQualityInlineFindings flag is true', async () => {
const wrapper = createWrapper(diffCodeQuality, {
glFeatures: { refactorCodeQualityInlineFindings: true },
});
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
await nextTick();
- expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(true);
- expect(wrapper.findComponent(DiffCodeQuality).props().codeQuality.length).not.toBe(0);
+ expect(wrapper.findComponent(DiffLine).props('line')).toBe(diffCodeQuality.diffLines[2]);
});
- it('does not render a codeQuality diff view when there is a finding & refactorCodeQualityInlineFindings flag is false ', async () => {
+ it('does not render a diff-line component when there is a finding & refactorCodeQualityInlineFindings flag is false', async () => {
const wrapper = createWrapper(diffCodeQuality, {
glFeatures: { refactorCodeQualityInlineFindings: false },
});
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
await nextTick();
- expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(false);
+ expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
});
it.each`
@@ -89,8 +88,8 @@ describe('DiffView', () => {
diffLines: [{ renderCommentRow: true, ...sides }],
inline: type === 'inline',
});
- expect(wrapper.findAll(DiffCommentCell).length).toBe(total);
- expect(wrapper.find(container).find(DiffCommentCell).exists()).toBe(true);
+ expect(wrapper.findAllComponents(DiffCommentCell).length).toBe(total);
+ expect(wrapper.find(container).findComponent(DiffCommentCell).exists()).toBe(true);
},
);
@@ -98,7 +97,7 @@ describe('DiffView', () => {
const wrapper = createWrapper({
diffLines: [{ renderCommentRow: true, left: { lineDraft: { isDraft: true } } }],
});
- expect(wrapper.find(DraftNote).exists()).toBe(true);
+ expect(wrapper.findComponent(DraftNote).exists()).toBe(true);
});
describe('drag operations', () => {
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 70191620eb6..ccf942bdcef 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -57,7 +57,7 @@ describe('Diffs image diff overlay component', () => {
it('renders icon when showCommentIcon is true', () => {
createComponent({ showCommentIcon: true });
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
it('sets badge comment positions', () => {
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index 6903b844e5e..dbfe9770e07 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -56,7 +56,7 @@ describe('Diff no changes empty state', () => {
it('Show create commit button', () => {
createComponent();
- expect(wrapper.find(GlButton).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(true);
});
it.each`
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 931a9562d36..ca7de8fd751 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -106,7 +106,7 @@ describe('Diffs tree list component', () => {
${'index.js'} | ${1}
${'app/*.js'} | ${1}
${'*.js, *.rb'} | ${2}
- `('it returns $itemSize item for $extension', async ({ extension, itemSize }) => {
+ `('returns $itemSize item for $extension', async ({ extension, itemSize }) => {
wrapper.find('[data-testid="diff-tree-search"]').setValue(extension);
await nextTick();
@@ -175,7 +175,7 @@ describe('Diffs tree list component', () => {
await nextTick();
// Have to use $attrs['viewed-files'] because we are passing down an object
// and attributes('') stringifies values (e.g. [object])...
- expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
+ expect(wrapper.findComponent(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
});
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js
index 6e99eadbd97..bead39ca744 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js
@@ -68,7 +68,7 @@ describe('Source Editor Toolbar', () => {
});
describe('buttons update', () => {
- it('it properly updates buttons on Apollo cache update', async () => {
+ it('properly updates buttons on Apollo cache update', async () => {
const item = buildButton('first', {
group: EDITOR_TOOLBAR_RIGHT_GROUP,
});
diff --git a/spec/frontend/editor/source_editor_extension_spec.js b/spec/frontend/editor/source_editor_extension_spec.js
index 78453aaa491..3424e71d326 100644
--- a/spec/frontend/editor/source_editor_extension_spec.js
+++ b/spec/frontend/editor/source_editor_extension_spec.js
@@ -16,7 +16,7 @@ describe('Editor Extension', () => {
'throws when definition = $definition and setupOptions = $setupOptions',
({ definition, setupOptions }) => {
const constructExtension = () => new EditorExtension({ definition, setupOptions });
- expect(constructExtension).toThrowError(EDITOR_EXTENSION_DEFINITION_ERROR);
+ expect(constructExtension).toThrow(EDITOR_EXTENSION_DEFINITION_ERROR);
},
);
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
index 1223fee320e..20ba23d56ff 100644
--- a/spec/frontend/editor/source_editor_instance_spec.js
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -248,7 +248,7 @@ describe('Source Editor Instance', () => {
const useExtension = () => {
seInstance.use(extensions);
};
- expect(useExtension).toThrowError(thrownError);
+ expect(useExtension).toThrow(thrownError);
},
);
@@ -336,7 +336,7 @@ describe('Source Editor Instance', () => {
const unuse = () => {
seInstance.unuse(unuseExtension);
};
- expect(unuse).toThrowError(thrownError);
+ expect(unuse).toThrow(thrownError);
},
);
@@ -382,7 +382,7 @@ describe('Source Editor Instance', () => {
},
);
- it('it does not remove entry from the global registry to keep for potential future re-use', () => {
+ it('does not remove entry from the global registry to keep for potential future re-use', () => {
const extensionStore = new Map();
seInstance = new SourceEditorInstance({}, extensionStore);
const extensions = seInstance.use(fullExtensionsArray);
diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js
index 096b6b1646f..f418eab668a 100644
--- a/spec/frontend/editor/source_editor_webide_ext_spec.js
+++ b/spec/frontend/editor/source_editor_webide_ext_spec.js
@@ -30,7 +30,7 @@ describe('Source Editor Web IDE Extension', () => {
const sideBySideSpy = jest.spyOn(instance, 'updateOptions');
instance.use({ definition: EditorWebIdeExtension });
- expect(sideBySideSpy).toBeCalledWith({ renderSideBySide });
+ expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide });
},
);
@@ -45,11 +45,11 @@ describe('Source Editor Web IDE Extension', () => {
const sideBySideSpy = jest.spyOn(instance, 'updateOptions');
await emitter.fire();
- expect(sideBySideSpy).toBeCalledWith({ renderSideBySide: true });
+ expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide: true });
editorEl.style.width = '0px';
await emitter.fire();
- expect(sideBySideSpy).toBeCalledWith({ renderSideBySide: false });
+ expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide: false });
});
});
});
diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js
index 82dc0cdc250..90816f28d5b 100644
--- a/spec/frontend/emoji/components/category_spec.js
+++ b/spec/frontend/emoji/components/category_spec.js
@@ -22,7 +22,7 @@ describe('Emoji category component', () => {
});
it('renders emoji groups', () => {
- expect(wrapper.findAll(EmojiGroup).length).toBe(2);
+ expect(wrapper.findAllComponents(EmojiGroup).length).toBe(2);
});
it('renders group', async () => {
@@ -30,19 +30,19 @@ describe('Emoji category component', () => {
// eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ renderGroup: true });
- expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
+ expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('renders group on appear', async () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
- expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
+ expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('emits appear event on appear', async () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
diff --git a/spec/frontend/emoji/components/utils_spec.js b/spec/frontend/emoji/components/utils_spec.js
index 56f514ee9a8..a17ddb3bb9a 100644
--- a/spec/frontend/emoji/components/utils_spec.js
+++ b/spec/frontend/emoji/components/utils_spec.js
@@ -4,13 +4,13 @@ import { getFrequentlyUsedEmojis, addToFrequentlyUsed } from '~/emoji/components
jest.mock('~/lib/utils/cookies');
describe('getFrequentlyUsedEmojis', () => {
- it('it returns null when no saved emojis set', () => {
+ it('returns null when no saved emojis set', () => {
jest.spyOn(Cookies, 'get').mockReturnValue(null);
expect(getFrequentlyUsedEmojis()).toBe(null);
});
- it('it returns frequently used emojis object', () => {
+ it('returns frequently used emojis object', () => {
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsup,thumbsdown');
expect(getFrequentlyUsedEmojis()).toEqual({
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index dc8f50e0e4b..36c3eeb5a52 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -120,177 +120,177 @@ describe('emoji', () => {
describe('isFlagEmoji', () => {
it('should gracefully handle empty string', () => {
- expect(isFlagEmoji('')).toBeFalsy();
+ expect(isFlagEmoji('')).toBe(false);
});
it('should detect flag_ac', () => {
- expect(isFlagEmoji('🇦🇨')).toBeTruthy();
+ expect(isFlagEmoji('🇦🇨')).toBe(true);
});
it('should detect flag_us', () => {
- expect(isFlagEmoji('🇺🇸')).toBeTruthy();
+ expect(isFlagEmoji('🇺🇸')).toBe(true);
});
it('should detect flag_zw', () => {
- expect(isFlagEmoji('🇿🇼')).toBeTruthy();
+ expect(isFlagEmoji('🇿🇼')).toBe(true);
});
it('should not detect flags', () => {
- expect(isFlagEmoji('🎏')).toBeFalsy();
+ expect(isFlagEmoji('🎏')).toBe(false);
});
it('should not detect triangular_flag_on_post', () => {
- expect(isFlagEmoji('🚩')).toBeFalsy();
+ expect(isFlagEmoji('🚩')).toBe(false);
});
it('should not detect single letter', () => {
- expect(isFlagEmoji('🇦')).toBeFalsy();
+ expect(isFlagEmoji('🇦')).toBe(false);
});
it('should not detect >2 letters', () => {
- expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
+ expect(isFlagEmoji('🇦🇧🇨')).toBe(false);
});
});
describe('isRainbowFlagEmoji', () => {
it('should gracefully handle empty string', () => {
- expect(isRainbowFlagEmoji('')).toBeFalsy();
+ expect(isRainbowFlagEmoji('')).toBe(false);
});
it('should detect rainbow_flag', () => {
- expect(isRainbowFlagEmoji('🏳🌈')).toBeTruthy();
+ expect(isRainbowFlagEmoji('🏳🌈')).toBe(true);
});
it("should not detect flag_white on its' own", () => {
- expect(isRainbowFlagEmoji('🏳')).toBeFalsy();
+ expect(isRainbowFlagEmoji('🏳')).toBe(false);
});
it("should not detect rainbow on its' own", () => {
- expect(isRainbowFlagEmoji('🌈')).toBeFalsy();
+ expect(isRainbowFlagEmoji('🌈')).toBe(false);
});
it('should not detect flag_white with something else', () => {
- expect(isRainbowFlagEmoji('🏳🔵')).toBeFalsy();
+ expect(isRainbowFlagEmoji('🏳🔵')).toBe(false);
});
});
describe('isKeycapEmoji', () => {
it('should gracefully handle empty string', () => {
- expect(isKeycapEmoji('')).toBeFalsy();
+ expect(isKeycapEmoji('')).toBe(false);
});
it('should detect one(keycap)', () => {
- expect(isKeycapEmoji('1️⃣')).toBeTruthy();
+ expect(isKeycapEmoji('1️⃣')).toBe(true);
});
it('should detect nine(keycap)', () => {
- expect(isKeycapEmoji('9️⃣')).toBeTruthy();
+ expect(isKeycapEmoji('9️⃣')).toBe(true);
});
it('should not detect ten(keycap)', () => {
- expect(isKeycapEmoji('🔟')).toBeFalsy();
+ expect(isKeycapEmoji('🔟')).toBe(false);
});
it('should not detect hash(keycap)', () => {
- expect(isKeycapEmoji('#⃣')).toBeFalsy();
+ expect(isKeycapEmoji('#⃣')).toBe(false);
});
});
describe('isSkinToneComboEmoji', () => {
it('should gracefully handle empty string', () => {
- expect(isSkinToneComboEmoji('')).toBeFalsy();
+ expect(isSkinToneComboEmoji('')).toBe(false);
});
it('should detect hand_splayed_tone5', () => {
- expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy();
+ expect(isSkinToneComboEmoji('🖐🏿')).toBe(true);
});
it('should not detect hand_splayed', () => {
- expect(isSkinToneComboEmoji('🖐')).toBeFalsy();
+ expect(isSkinToneComboEmoji('🖐')).toBe(false);
});
it('should detect lifter_tone1', () => {
- expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy();
+ expect(isSkinToneComboEmoji('🏋🏻')).toBe(true);
});
it('should not detect lifter', () => {
- expect(isSkinToneComboEmoji('🏋')).toBeFalsy();
+ expect(isSkinToneComboEmoji('🏋')).toBe(false);
});
it('should detect rowboat_tone4', () => {
- expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy();
+ expect(isSkinToneComboEmoji('🚣🏾')).toBe(true);
});
it('should not detect rowboat', () => {
- expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
+ expect(isSkinToneComboEmoji('🚣')).toBe(false);
});
it('should not detect individual tone emoji', () => {
- expect(isSkinToneComboEmoji('🏻')).toBeFalsy();
+ expect(isSkinToneComboEmoji('🏻')).toBe(false);
});
});
describe('isHorceRacingSkinToneComboEmoji', () => {
it('should gracefully handle empty string', () => {
- expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy();
+ expect(isHorceRacingSkinToneComboEmoji('')).toBeUndefined();
});
it('should detect horse_racing_tone2', () => {
- expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy();
+ expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBe(true);
});
it('should not detect horse_racing', () => {
- expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy();
+ expect(isHorceRacingSkinToneComboEmoji('🏇')).toBe(false);
});
});
describe('isPersonZwjEmoji', () => {
it('should gracefully handle empty string', () => {
- expect(isPersonZwjEmoji('')).toBeFalsy();
+ expect(isPersonZwjEmoji('')).toBe(false);
});
it('should detect couple_mm', () => {
- expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy();
+ expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBe(true);
});
it('should not detect couple_with_heart', () => {
- expect(isPersonZwjEmoji('💑')).toBeFalsy();
+ expect(isPersonZwjEmoji('💑')).toBe(false);
});
it('should not detect couplekiss', () => {
- expect(isPersonZwjEmoji('💏')).toBeFalsy();
+ expect(isPersonZwjEmoji('💏')).toBe(false);
});
it('should detect family_mmb', () => {
- expect(isPersonZwjEmoji('👨‍👨‍👦')).toBeTruthy();
+ expect(isPersonZwjEmoji('👨‍👨‍👦')).toBe(true);
});
it('should detect family_mwgb', () => {
- expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBeTruthy();
+ expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBe(true);
});
it('should not detect family', () => {
- expect(isPersonZwjEmoji('👪')).toBeFalsy();
+ expect(isPersonZwjEmoji('👪')).toBe(false);
});
it('should detect kiss_ww', () => {
- expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBeTruthy();
+ expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBe(true);
});
it('should not detect girl', () => {
- expect(isPersonZwjEmoji('👧')).toBeFalsy();
+ expect(isPersonZwjEmoji('👧')).toBe(false);
});
it('should not detect girl_tone5', () => {
- expect(isPersonZwjEmoji('👧🏿')).toBeFalsy();
+ expect(isPersonZwjEmoji('👧🏿')).toBe(false);
});
it('should not detect man', () => {
- expect(isPersonZwjEmoji('👨')).toBeFalsy();
+ expect(isPersonZwjEmoji('👨')).toBe(false);
});
it('should not detect woman', () => {
- expect(isPersonZwjEmoji('👩')).toBeFalsy();
+ expect(isPersonZwjEmoji('👩')).toBe(false);
});
});
@@ -298,13 +298,13 @@ describe('emoji', () => {
it('should gracefully handle empty string with unicode support', () => {
const isSupported = isEmojiUnicodeSupported({ '1.0': true }, '', '1.0');
- expect(isSupported).toBeTruthy();
+ expect(isSupported).toBe(true);
});
it('should gracefully handle empty string without unicode support', () => {
const isSupported = isEmojiUnicodeSupported({}, '', '1.0');
- expect(isSupported).toBeFalsy();
+ expect(isSupported).toBeUndefined();
});
it('bomb(6.0) with 6.0 support', () => {
@@ -316,7 +316,7 @@ describe('emoji', () => {
emojiFixtureMap[emojiKey].unicodeVersion,
);
- expect(isSupported).toBeTruthy();
+ expect(isSupported).toBe(true);
});
it('bomb(6.0) without 6.0 support', () => {
@@ -328,7 +328,7 @@ describe('emoji', () => {
emojiFixtureMap[emojiKey].unicodeVersion,
);
- expect(isSupported).toBeFalsy();
+ expect(isSupported).toBe(false);
});
it('bomb(6.0) without 6.0 but with 9.0 support', () => {
@@ -340,7 +340,7 @@ describe('emoji', () => {
emojiFixtureMap[emojiKey].unicodeVersion,
);
- expect(isSupported).toBeFalsy();
+ expect(isSupported).toBe(false);
});
it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
@@ -367,7 +367,7 @@ describe('emoji', () => {
emojiFixtureMap[emojiKey].unicodeVersion,
);
- expect(isSupported).toBeFalsy();
+ expect(isSupported).toBe(false);
});
it('use native keycap on >=57 chrome', () => {
@@ -386,7 +386,7 @@ describe('emoji', () => {
emojiFixtureMap[emojiKey].unicodeVersion,
);
- expect(isSupported).toBeTruthy();
+ expect(isSupported).toBe(true);
});
it('fallback keycap on <57 chrome', () => {
@@ -405,7 +405,7 @@ describe('emoji', () => {
emojiFixtureMap[emojiKey].unicodeVersion,
);
- expect(isSupported).toBeFalsy();
+ expect(isSupported).toBe(false);
});
});
diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js
index 6cc363e000b..4cbbb60b74c 100644
--- a/spec/frontend/environments/deployment_spec.js
+++ b/spec/frontend/environments/deployment_spec.js
@@ -1,4 +1,6 @@
-import { GlCollapse } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import { stubTransition } from 'helpers/stub_transition';
@@ -8,9 +10,13 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Deployment from '~/environments/components/deployment.vue';
import Commit from '~/environments/components/commit.vue';
import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
-import { resolvedEnvironment } from './graphql/mock_data';
+import createMockApollo from '../__helpers__/mock_apollo_helper';
+import waitForPromises from '../__helpers__/wait_for_promises';
+import getDeploymentDetails from '../../../app/assets/javascripts/environments/graphql/queries/deployment_details.query.graphql';
+import { resolvedEnvironment, resolvedDeploymentDetails } from './graphql/mock_data';
describe('~/environments/components/deployment.vue', () => {
+ Vue.use(VueApollo);
useFakeDate(2022, 0, 8, 16);
let deployment;
@@ -20,14 +26,23 @@ describe('~/environments/components/deployment.vue', () => {
deployment = resolvedEnvironment.lastDeployment;
});
- const createWrapper = ({ propsData = {} } = {}) =>
- mountExtended(Deployment, {
+ const createWrapper = ({ propsData = {}, options = {} } = {}) => {
+ const mockApollo = createMockApollo([
+ [getDeploymentDetails, jest.fn().mockResolvedValue(resolvedDeploymentDetails)],
+ ]);
+
+ return mountExtended(Deployment, {
+ stubs: { transition: stubTransition() },
propsData: {
deployment,
+ visible: true,
...propsData,
},
- stubs: { transition: stubTransition() },
+ apolloProvider: mockApollo,
+ provide: { projectPath: '/1' },
+ ...options,
});
+ };
afterEach(() => {
wrapper?.destroy();
@@ -102,10 +117,11 @@ describe('~/environments/components/deployment.vue', () => {
});
it('shows the short SHA for the commit of the deployment', () => {
- const sha = wrapper.findByTitle(__('Commit SHA'));
+ const sha = wrapper.findByRole('link', { name: __('Commit SHA') });
expect(sha.exists()).toBe(true);
expect(sha.text()).toBe(deployment.commit.shortId);
+ expect(sha.attributes('href')).toBe(deployment.commit.commitPath);
});
it('shows the commit icon', () => {
@@ -183,29 +199,12 @@ describe('~/environments/components/deployment.vue', () => {
});
});
- describe('collapse', () => {
- let collapse;
- let button;
-
+ describe('details', () => {
beforeEach(() => {
wrapper = createWrapper();
- collapse = wrapper.findComponent(GlCollapse);
- button = wrapper.findComponent({ ref: 'details-toggle' });
});
- it('is collapsed by default', () => {
- expect(collapse.attributes('visible')).toBeUndefined();
- expect(button.props('icon')).toBe('expand-down');
- expect(button.text()).toBe(__('Show details'));
- });
-
- it('opens on click', async () => {
- await button.trigger('click');
-
- expect(button.text()).toBe(__('Hide details'));
- expect(button.props('icon')).toBe('expand-up');
- expect(collapse.attributes('visible')).toBe('visible');
-
+ it('shows information about the deployment', () => {
const username = wrapper.findByRole('link', { name: `@${deployment.user.username}` });
expect(username.attributes('href')).toBe(deployment.user.path);
@@ -221,24 +220,43 @@ describe('~/environments/components/deployment.vue', () => {
const ref = wrapper.findByRole('link', { name: deployment.ref.name });
expect(ref.attributes('href')).toBe(deployment.ref.refPath);
});
+
+ it('shows information about tags related to the deployment', async () => {
+ expect(wrapper.findByText(__('Tags')).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+
+ await waitForPromises();
+
+ for (let i = 1; i < 6; i += 1) {
+ const tagName = __(`testTag${i}`);
+ const testTag = wrapper.findByText(tagName);
+ expect(testTag.exists()).toBe(true);
+ expect(testTag.attributes('href')).toBe(`tags/${tagName}`);
+ }
+ expect(wrapper.findByText(__('testTag6')).exists()).toBe(false);
+ expect(wrapper.findByText(__('Tag')).exists()).toBe(false);
+ // with more than 5 tags, show overflow marker
+ expect(wrapper.findByText('...').exists()).toBe(true);
+ });
});
describe('with tagged deployment', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } });
- await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
});
- it('shows tag instead of branch', () => {
- const refLabel = wrapper.findByText(__('Tag'));
+ it('shows tags instead of branch', () => {
+ const refLabel = wrapper.findByText(__('Tags'));
expect(refLabel.exists()).toBe(true);
+
+ const branchLabel = wrapper.findByText(__('Branch'));
+ expect(branchLabel.exists()).toBe(false);
});
});
describe('with API deployment', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
- await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
});
it('shows API instead of a job name', () => {
@@ -247,13 +265,12 @@ describe('~/environments/components/deployment.vue', () => {
});
});
describe('without a job path', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createWrapper({
propsData: {
deployment: { ...deployment, deployable: { name: deployment.deployable.name } },
},
});
- await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
});
it('shows a span instead of a link', () => {
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index 49a643aaac8..a86cfdd56ba 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -363,7 +363,7 @@ describe('Environment table', () => {
});
describe('sortedEnvironments', () => {
- it('it should sort children as well', () => {
+ it('should sort children as well', () => {
const mockItems = [
{
name: 'production',
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 57f98c81124..aff54107d6b 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -50,6 +50,7 @@ describe('~/environments/components/environments_app.vue', () => {
defaultBranchName: 'main',
helpPagePath: '/help',
projectId: '1',
+ projectPath: '/1',
...provide,
},
apolloProvider,
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 7e436476a8f..d246641b94b 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -757,3 +757,41 @@ export const resolvedFolder = {
stoppedCount: 0,
__typename: 'LocalEnvironmentFolder',
};
+
+export const resolvedDeploymentDetails = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ deployment: {
+ id: 'gid://gitlab/Deployment/99',
+ iid: '55',
+ tags: [
+ {
+ name: 'testTag1',
+ path: 'tags/testTag1',
+ },
+ {
+ name: 'testTag2',
+ path: 'tags/testTag2',
+ },
+ {
+ name: 'testTag3',
+ path: 'tags/testTag3',
+ },
+ {
+ name: 'testTag4',
+ path: 'tags/testTag4',
+ },
+ {
+ name: 'testTag5',
+ path: 'tags/testTag5',
+ },
+ {
+ name: 'testTag6',
+ path: 'tags/testTag6',
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index a151595bf64..76cd09cfb4e 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -24,7 +24,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
- provide: { helpPagePath: '/help', projectId: '1' },
+ provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' },
stubs: { transition: stubTransition() },
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index 5a1c1c7714c..2405cb82eac 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -65,7 +65,7 @@ describe('~/environments/components/new.vue', () => {
input | value
${() => name} | ${'test'}
${() => url} | ${'https://example.org'}
- `('it changes the value of the input to $value', async ({ input, value }) => {
+ `('changes the value of the input to $value', async ({ input, value }) => {
await input().setValue(value);
expect(input().element.value).toBe(value);
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index b7dffbbec04..805ada54509 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -164,19 +164,19 @@ describe('ErrorTrackingList', () => {
expect(findSortDropdown().exists()).toBe(true);
});
- it('it searches by query', () => {
+ it('searches by query', () => {
findSearchBox().vm.$emit('input', 'search');
findSearchBox().trigger('keyup.enter');
expect(actions.searchByQuery.mock.calls[0][1]).toBe('search');
});
- it('it sorts by fields', () => {
+ it('sorts by fields', () => {
const findSortItem = () => findSortDropdown().find('.dropdown-item');
findSortItem().trigger('click');
expect(actions.sortByField).toHaveBeenCalled();
});
- it('it filters by status', () => {
+ it('filters by status', () => {
const findStatusFilter = () => findStatusFilterDropdown().find('.dropdown-item');
findStatusFilter().trigger('click');
expect(actions.filterByStatus).toHaveBeenCalled();
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index 693fcff50ca..0de4277b08a 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -62,7 +62,7 @@ describe('Stacktrace Entry', () => {
);
});
- it('should render only lineNo:columnNO when there is no errorFn ', () => {
+ it('should render only lineNo:columnNO when there is no errorFn', () => {
const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 };
mountComponent({ expanded: false, lines: [], ...extraInfo });
const fileHeaderContent = trimText(findFileHeaderContent());
@@ -70,7 +70,7 @@ describe('Stacktrace Entry', () => {
expect(fileHeaderContent).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`);
});
- it('should render only lineNo when there is no errorColumn ', () => {
+ it('should render only lineNo when there is no errorColumn', () => {
const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null };
mountComponent({ expanded: false, lines: [], ...extraInfo });
const fileHeaderContent = trimText(findFileHeaderContent());
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index c660c9c4a99..7a714cc1ebc 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -76,23 +76,23 @@ describe('error tracking settings app', () => {
describe('section', () => {
it('renders the form and dropdown', () => {
- expect(wrapper.find(ErrorTrackingForm).exists()).toBeTruthy();
- expect(wrapper.find(ProjectDropdown).exists()).toBeTruthy();
+ expect(wrapper.findComponent(ErrorTrackingForm).exists()).toBe(true);
+ expect(wrapper.findComponent(ProjectDropdown).exists()).toBe(true);
});
it('renders the Save Changes button', () => {
- expect(wrapper.find('.js-error-tracking-button').exists()).toBeTruthy();
+ expect(wrapper.find('.js-error-tracking-button').exists()).toBe(true);
});
it('enables the button by default', () => {
- expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy();
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeUndefined();
});
it('disables the button when saving', async () => {
store.state.settingsLoading = true;
await nextTick();
- expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy();
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBe('true');
});
});
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index b44af547658..c9095441d41 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('shows helper text', () => {
@@ -57,8 +57,8 @@ describe('error tracking settings project dropdown', () => {
});
it('does not contain any dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBe(false);
- expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available');
});
});
@@ -71,12 +71,12 @@ describe('error tracking settings project dropdown', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('contains a number of dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
- expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2);
});
});
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index e8103df78bc..2b9710c9085 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -8,7 +8,7 @@ import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdo
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
-describe('Feature flags > Environments dropdown ', () => {
+describe('Feature flags > Environments dropdown', () => {
let wrapper;
let mock;
const results = ['production', 'staging'];
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js
index b6114cb0c9f..7132e83a940 100644
--- a/spec/frontend/feature_flags/store/edit/actions_spec.js
+++ b/spec/frontend/feature_flags/store/edit/actions_spec.js
@@ -40,7 +40,7 @@ describe('Feature flags Edit Module actions', () => {
});
describe('success', () => {
- it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', () => {
+ it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
const featureFlag = {
name: 'name',
description: 'description',
@@ -75,7 +75,7 @@ describe('Feature flags Edit Module actions', () => {
});
describe('error', () => {
- it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError ', () => {
+ it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError', () => {
mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] });
return testAction(
@@ -154,7 +154,7 @@ describe('Feature flags Edit Module actions', () => {
});
describe('success', () => {
- it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess ', () => {
+ it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 1 });
return testAction(
@@ -176,7 +176,7 @@ describe('Feature flags Edit Module actions', () => {
});
describe('error', () => {
- it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError ', () => {
+ it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
return testAction(
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index ce62c3b0473..96a7d868316 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -56,7 +56,7 @@ describe('Feature flags actions', () => {
});
describe('success', () => {
- it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess ', () => {
+ it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {});
return testAction(
@@ -78,7 +78,7 @@ describe('Feature flags actions', () => {
});
describe('error', () => {
- it('dispatches requestFeatureFlags and receiveFeatureFlagsError ', () => {
+ it('dispatches requestFeatureFlags and receiveFeatureFlagsError', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
return testAction(
@@ -153,7 +153,7 @@ describe('Feature flags actions', () => {
});
describe('success', () => {
- it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess ', () => {
+ it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess', () => {
mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(200, rotateData, {});
return testAction(
@@ -175,7 +175,7 @@ describe('Feature flags actions', () => {
});
describe('error', () => {
- it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError ', () => {
+ it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
return testAction(
diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js
index 1dcd2da1d93..dbe6669c868 100644
--- a/spec/frontend/feature_flags/store/new/actions_spec.js
+++ b/spec/frontend/feature_flags/store/new/actions_spec.js
@@ -33,7 +33,7 @@ describe('Feature flags New Module Actions', () => {
});
describe('success', () => {
- it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', () => {
+ it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess', () => {
const actionParams = {
name: 'name',
description: 'description',
@@ -68,7 +68,7 @@ describe('Feature flags New Module Actions', () => {
});
describe('error', () => {
- it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', () => {
+ it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError', () => {
const actionParams = {
name: 'name',
description: 'description',
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index 897ad5ee2bf..91457f10bf8 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
import eventHub from '~/filtered_search/event_hub';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
@@ -6,12 +6,12 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
describe('Recent Searches Dropdown Content', () => {
let wrapper;
- const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' });
- const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' });
- const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' });
+ const findLocalStorageNote = () => wrapper.findByTestId('local-storage-note');
+ const findDropdownItems = () => wrapper.findAllByTestId('dropdown-item');
+ const findDropdownNote = () => wrapper.findByTestId('dropdown-note');
const createComponent = (props) => {
- wrapper = shallowMount(RecentSearchesDropdownContent, {
+ wrapper = shallowMountExtended(RecentSearchesDropdownContent, {
propsData: {
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
items: [],
@@ -94,7 +94,7 @@ describe('Recent Searches Dropdown Content', () => {
});
it('emits requestClearRecentSearches on Clear resent searches button', () => {
- wrapper.find({ ref: 'clearButton' }).trigger('click');
+ wrapper.findByTestId('clear-button').trigger('click');
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/filtered_search/droplab/drop_down_spec.js b/spec/frontend/filtered_search/droplab/drop_down_spec.js
index f49dbfcf79c..6fbb4394944 100644
--- a/spec/frontend/filtered_search/droplab/drop_down_spec.js
+++ b/spec/frontend/filtered_search/droplab/drop_down_spec.js
@@ -557,11 +557,11 @@ describe('DropLab DropDown', () => {
DropDown.prototype.show.call(testContext.dropdown);
});
- it('it should set .list display to block', () => {
+ it('should set .list display to block', () => {
expect(testContext.list.style.display).toBe('block');
});
- it('it should set .hidden to false', () => {
+ it('should set .hidden to false', () => {
expect(testContext.dropdown.hidden).toBe(false);
});
@@ -591,11 +591,11 @@ describe('DropLab DropDown', () => {
DropDown.prototype.hide.call(testContext.dropdown);
});
- it('it should set .list display to none', () => {
+ it('should set .list display to none', () => {
expect(testContext.list.style.display).toBe('none');
});
- it('it should set .hidden to true', () => {
+ it('should set .hidden to true', () => {
expect(testContext.dropdown.hidden).toBe(true);
});
});
@@ -648,11 +648,11 @@ describe('DropLab DropDown', () => {
DropDown.prototype.destroy.call(testContext.dropdown);
});
- it('it should call .hide', () => {
+ it('should call .hide', () => {
expect(testContext.dropdown.hide).toHaveBeenCalled();
});
- it('it should call .removeEventListener', () => {
+ it('should call .removeEventListener', () => {
expect(testContext.list.removeEventListener).toHaveBeenCalledWith(
'click',
testContext.eventWrapper.clickEvent,
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index 75bc8c8df25..7d95c506e6c 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -7,7 +7,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let_it_be(:admin) { create(:admin, name: 'root') }
- let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' )}
+ let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' ) }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let_it_be(:early_mrs) do
4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index eada2f8e0f7..5acc1095d5c 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -7,7 +7,7 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin, name: 'root') }
- let(:namespace) { create(:namespace, name: 'gitlab-test' )}
+ let(:namespace) { create(:namespace, name: 'gitlab-test' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index a7a989f31ec..b3ce23c8cd7 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -8,7 +8,7 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
include AdminModeHelper
let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') }
before do
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index b2bbdd2749e..54c5b83da3e 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index b3bb4b8873a..6cda2f0f665 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Branches (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let_it_be(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index 49596d98774..426a76f29e0 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace) }
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb
index 154084e0181..24d602216d8 100644
--- a/spec/frontend/fixtures/deploy_keys.rb
+++ b/spec/frontend/fixtures/deploy_keys.rb
@@ -7,11 +7,11 @@ RSpec.describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :c
include AdminModeHelper
let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
- let(:project2) { create(:project, :internal)}
- let(:project3) { create(:project, :internal)}
- let(:project4) { create(:project, :internal)}
+ let(:project2) { create(:project, :internal) }
+ let(:project3) { create(:project, :internal) }
+ let(:project4) { create(:project, :internal) }
before do
# Using an admin for these fixtures because they are used for verifying a frontend
diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb
index ddd436b98c6..9c22ff176ff 100644
--- a/spec/frontend/fixtures/groups.rb
+++ b/spec/frontend/fixtures/groups.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:user) { create(:user) }
- let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')}
+ let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre') }
before do
group.add_owner(user)
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index cde796497d4..e3d88098841 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
include JavaScriptFixturesHelpers
let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
render_views
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 2e15eefdce6..3657a5405a4 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
let(:user) { project.first_owner }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb
index 6736baed199..2445c9376e2 100644
--- a/spec/frontend/fixtures/labels.rb
+++ b/spec/frontend/fixtures/labels.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Labels (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let(:user) { create(:user) }
- let(:group) { create(:group, name: 'frontend-fixtures-group' )}
+ let(:group) { create(:group, name: 'frontend-fixtures-group' ) }
let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') }
let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') }
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index cb4eb43b88d..cbf26a70e5f 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index 7f0d650b710..ff4b27844a6 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, description: '- [ ] Task List Item') }
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
index d59b01b04af..7f8b3d378d3 100644
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ b/spec/frontend/fixtures/metrics_dashboard.rb
@@ -7,7 +7,7 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:namespace) { create(:namespace, name: 'monitoring' )}
+ let_it_be(:namespace) { create(:namespace, name: 'monitoring' ) }
let_it_be(:project) { project_with_dashboard_namespace('.gitlab/dashboards/test.yml', nil, namespace: namespace) }
let_it_be(:environment) { create(:environment, id: 1, project: project) }
let_it_be(:params) { { environment: environment } }
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index e155d27920d..5b7a445557e 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 709e14183df..114db26d6a9 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') }
let_it_be(:commit_without_author) { RepoHelpers.another_sample_commit }
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index fa7d61df3e8..b9c427c7505 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
runners_token = 'runnerstoken:intabulasreferre'
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) }
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index b117cfea5fa..7bd5b8c5f6c 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Raw files', '(JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') }
let(:response) { @response }
diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb
index db1ef67998f..b2da383d657 100644
--- a/spec/frontend/fixtures/search.rb
+++ b/spec/frontend/fixtures/search.rb
@@ -23,40 +23,41 @@ RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :public, :repository, namespace: namespace, path: 'search-project') }
let(:blobs) do
- Kaminari.paginate_array([
- Gitlab::Search::FoundBlob.new(
- path: 'CHANGELOG',
- basename: 'CHANGELOG',
- ref: 'master',
- data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat",
- project: project,
- project_id: project.id,
- startline: 2),
- Gitlab::Search::FoundBlob.new(
- path: 'CONTRIBUTING',
- basename: 'CONTRIBUTING',
- ref: 'master',
- data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat",
- project: project,
- project_id: project.id,
- startline: 2),
- Gitlab::Search::FoundBlob.new(
- path: 'README',
- basename: 'README',
- ref: 'master',
- data: "foo\nSend # this is the highlight\nbaz\nboo\nbat",
- project: project,
- project_id: project.id,
- startline: 2),
- Gitlab::Search::FoundBlob.new(
- path: 'test',
- basename: 'test',
- ref: 'master',
- data: "foo\nSend # this is the highlight\nbaz\nboo\nbat",
- project: project,
- project_id: project.id,
- startline: 2)
- ],
+ Kaminari.paginate_array(
+ [
+ Gitlab::Search::FoundBlob.new(
+ path: 'CHANGELOG',
+ basename: 'CHANGELOG',
+ ref: 'master',
+ data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat",
+ project: project,
+ project_id: project.id,
+ startline: 2),
+ Gitlab::Search::FoundBlob.new(
+ path: 'CONTRIBUTING',
+ basename: 'CONTRIBUTING',
+ ref: 'master',
+ data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat",
+ project: project,
+ project_id: project.id,
+ startline: 2),
+ Gitlab::Search::FoundBlob.new(
+ path: 'README',
+ basename: 'README',
+ ref: 'master',
+ data: "foo\nSend # this is the highlight\nbaz\nboo\nbat",
+ project: project,
+ project_id: project.id,
+ startline: 2),
+ Gitlab::Search::FoundBlob.new(
+ path: 'test',
+ basename: 'test',
+ ref: 'master',
+ data: "foo\nSend # this is the highlight\nbaz\nboo\nbat",
+ project: project,
+ project_id: project.id,
+ startline: 2)
+ ],
total_count: 4,
limit: 4,
offset: 0)
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index f05ff3ee269..58d4bc5c1f3 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:user) { project.first_owner }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index cf7383fa6ca..bd2d63a1827 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -69,11 +69,25 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
it_behaves_like 'startup css project fixtures', 'dark'
end
- describe RegistrationsController, '(Startup CSS fixtures)', type: :controller do
+ describe SessionsController, '(Startup CSS fixtures)', type: :controller do
+ include DeviseHelpers
+
+ before do
+ set_devise_mapping(context: request)
+ end
+
it 'startup_css/sign-in.html' do
get :new
expect(response).to be_successful
end
+
+ it 'startup_css/sign-in-old.html' do
+ stub_feature_flags(restyle_login_page: false)
+
+ get :new
+
+ expect(response).to be_successful
+ end
end
end
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index 7dce09e8f49..d934396f803 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Todos (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:user) { project.first_owner }
let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 6cd32ff6b40..e26c52f0bf7 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -36,7 +36,7 @@ describe('Flash', () => {
hideFlash(el, false);
expect(el.style.opacity).toBe('');
- expect(el.style.transition).toBeFalsy();
+ expect(el.style.transition).toHaveLength(0);
});
it('removes element after transitionend', () => {
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index eef5dc86c1a..e6673fa78ec 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,7 +1,7 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
@@ -16,18 +16,18 @@ describe('FrequentItemsListItemComponent', () => {
let trackingSpy;
let store;
- const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
+ const findTitle = () => wrapper.findByTestId('frequent-items-item-title');
const findAvatar = () => wrapper.findComponent(ProjectAvatar);
- const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
- const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
+ const findAllTitles = () => wrapper.findAllByTestId('frequent-items-item-title');
+ const findNamespace = () => wrapper.findByTestId('frequent-items-item-namespace');
const findAllButtons = () => wrapper.findAllComponents(GlButton);
- const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
+ const findAllNamespace = () => wrapper.findAllByTestId('frequent-items-item-namespace');
const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar);
const findAllMetadataContainers = () =>
- wrapper.findAll({ ref: 'frequentItemsItemMetadataContainer' });
+ wrapper.findAllByTestId('frequent-items-item-metadata-container');
const createComponent = (props = {}) => {
- wrapper = shallowMount(frequentItemsListItemComponent, {
+ wrapper = shallowMountExtended(frequentItemsListItemComponent, {
store,
propsData: {
itemId: mockProject.id,
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
index beaab1913d0..9f08a432a3d 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -1,6 +1,6 @@
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { createStore } from '~/frequent_items/store';
@@ -12,7 +12,7 @@ describe('FrequentItemsListComponent', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = mount(frequentItemsListComponent, {
+ wrapper = mountExtended(frequentItemsListComponent, {
store: createStore(),
propsData: {
namespace: 'projects',
@@ -94,8 +94,8 @@ describe('FrequentItemsListComponent', () => {
await nextTick();
expect(wrapper.classes('frequent-items-list-container')).toBe(true);
- expect(wrapper.findAll({ ref: 'frequentItemsList' })).toHaveLength(1);
- expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(5);
+ expect(wrapper.findAllByTestId('frequent-items-list')).toHaveLength(1);
+ expect(wrapper.findAllComponents(frequentItemsListItemComponent)).toHaveLength(5);
});
it('should render component element with empty message', async () => {
@@ -105,7 +105,7 @@ describe('FrequentItemsListComponent', () => {
await nextTick();
expect(wrapper.vm.$el.querySelectorAll('li.section-empty')).toHaveLength(1);
- expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(0);
+ expect(wrapper.findAllComponents(frequentItemsListItemComponent)).toHaveLength(0);
});
});
});
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index d0a4cf70f5f..94fc97b82c2 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -23,7 +23,7 @@ describe('FrequentItemsSearchInputComponent', () => {
},
});
- const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js
index 33c655a6ffd..8d4c89bd48f 100644
--- a/spec/frontend/frequent_items/utils_spec.js
+++ b/spec/frontend/frequent_items/utils_spec.js
@@ -10,25 +10,25 @@ import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_
describe('Frequent Items utils spec', () => {
describe('isMobile', () => {
- it('returns true when the screen is medium ', () => {
+ it('returns true when the screen is medium', () => {
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
expect(isMobile()).toBe(true);
});
- it('returns true when the screen is small ', () => {
+ it('returns true when the screen is small', () => {
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm');
expect(isMobile()).toBe(true);
});
- it('returns true when the screen is extra-small ', () => {
+ it('returns true when the screen is extra-small', () => {
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs');
expect(isMobile()).toBe(true);
});
- it('returns false when the screen is larger than medium ', () => {
+ it('returns false when the screen is larger than medium', () => {
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg');
expect(isMobile()).toBe(false);
diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js
index 490c0136651..e6a0d74f348 100644
--- a/spec/frontend/google_cloud/databases/panel_spec.js
+++ b/spec/frontend/google_cloud/databases/panel_spec.js
@@ -2,6 +2,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Panel from '~/google_cloud/databases/panel.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+import ServiceTable from '~/google_cloud/databases/service_table.vue';
+import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue';
describe('google_cloud/databases/panel', () => {
let wrapper;
@@ -10,6 +12,11 @@ describe('google_cloud/databases/panel', () => {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
+ cloudsqlPostgresUrl: 'cloudsql-postgres-url',
+ cloudsqlMysqlUrl: 'cloudsql-mysql-url',
+ cloudsqlSqlserverUrl: 'cloudsql-sqlserver-url',
+ cloudsqlInstances: [],
+ emptyIllustrationUrl: 'empty-illustration-url',
};
beforeEach(() => {
@@ -33,4 +40,14 @@ describe('google_cloud/databases/panel', () => {
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
});
+
+ it('contains Databases service table', () => {
+ const target = wrapper.findComponent(ServiceTable);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains CloudSQL instance table', () => {
+ const target = wrapper.findComponent(InstanceTable);
+ expect(target.exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
index 6a7eb1fd9f1..ec9e1ef8e5f 100644
--- a/spec/frontend/google_tag_manager/index_spec.js
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -8,13 +8,13 @@ import {
trackSaasTrialSubmit,
trackSaasTrialSkip,
trackSaasTrialGroup,
- trackSaasTrialProject,
trackSaasTrialGetStarted,
trackTrialAcceptTerms,
trackCheckout,
trackTransaction,
trackAddToCartUsageTab,
getNamespaceId,
+ trackCompanyForm,
} from '~/google_tag_manager';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { logError } from '~/lib/logger';
@@ -149,9 +149,6 @@ describe('~/google_tag_manager/index', () => {
createTestCase(trackSaasTrialGroup, {
forms: [{ cls: 'js-saas-trial-group', expectation: { event: 'saasTrialGroup' } }],
}),
- createTestCase(trackSaasTrialProject, {
- forms: [{ id: 'new_project', expectation: { event: 'saasTrialProject' } }],
- }),
createTestCase(trackProjectImport, {
links: [
{
@@ -440,6 +437,34 @@ describe('~/google_tag_manager/index', () => {
});
});
});
+
+ describe('when trackCompanyForm is invoked', () => {
+ it('with an ultimate trial', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackCompanyForm('ultimate_trial');
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith({
+ event: 'aboutYourCompanyFormSubmit',
+ aboutYourCompanyType: 'ultimate_trial',
+ });
+ expect(logError).not.toHaveBeenCalled();
+ });
+
+ it('with a free account', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackCompanyForm('free_account');
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith({
+ event: 'aboutYourCompanyFormSubmit',
+ aboutYourCompanyType: 'free_account',
+ });
+ expect(logError).not.toHaveBeenCalled();
+ });
+ });
});
describe.each([
@@ -452,11 +477,11 @@ describe('~/google_tag_manager/index', () => {
});
it('no ops', () => {
- setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] }));
+ setHTMLFixture(createHTML({ forms: [{ cls: 'js-saas-trial-group' }] }));
- trackSaasTrialProject();
+ trackSaasTrialGroup();
- triggerEvent('#new_project', 'submit');
+ triggerEvent('.js-saas-trial-group', 'submit');
expect(spy).not.toHaveBeenCalled();
expect(logError).not.toHaveBeenCalled();
@@ -477,11 +502,11 @@ describe('~/google_tag_manager/index', () => {
});
it('logs error', () => {
- setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] }));
+ setHTMLFixture(createHTML({ forms: [{ cls: 'js-saas-trial-group' }] }));
- trackSaasTrialProject();
+ trackSaasTrialGroup();
- triggerEvent('#new_project', 'submit');
+ triggerEvent('.js-saas-trial-group', 'submit');
expect(logError).toHaveBeenCalledWith(
'Unexpected error while pushing to dataLayer',
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index a6bbea648d2..a4a7530184d 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -40,7 +40,7 @@ describe('AppComponent', () => {
const store = new GroupsStore({ hideProjects: false });
const service = new GroupsService(mockEndpoint);
- const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => {
+ const createShallowComponent = ({ propsData = {} } = {}) => {
store.state.pageInfo = mockPageInfo;
wrapper = shallowMount(appComponent, {
propsData: {
@@ -53,10 +53,6 @@ describe('AppComponent', () => {
mocks: {
$toast,
},
- provide: {
- renderEmptyState: false,
- ...provide,
- },
});
vm = wrapper.vm;
};
@@ -402,8 +398,7 @@ describe('AppComponent', () => {
({ action, groups, fromSearch, renderEmptyState, expected }) => {
it(expected ? 'renders empty state' : 'does not render empty state', async () => {
createShallowComponent({
- propsData: { action },
- provide: { renderEmptyState },
+ propsData: { action, renderEmptyState },
});
vm.updateGroups(groups, fromSearch);
@@ -420,7 +415,6 @@ describe('AppComponent', () => {
it('renders legacy empty state', async () => {
createShallowComponent({
propsData: { action: 'subgroups_and_projects' },
- provide: { renderEmptyState: false },
});
vm.updateGroups([], false);
@@ -481,7 +475,7 @@ describe('AppComponent', () => {
it('should render loading icon', async () => {
vm.isLoading = true;
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('should render groups tree', async () => {
@@ -494,7 +488,7 @@ describe('AppComponent', () => {
it('renders modal confirmation dialog', () => {
createShallowComponent();
- const findGlModal = wrapper.find(GlModal);
+ const findGlModal = wrapper.findComponent(GlModal);
expect(findGlModal.exists()).toBe(true);
expect(findGlModal.attributes('title')).toBe('Are you sure?');
diff --git a/spec/frontend/groups/components/empty_state_spec.js b/spec/frontend/groups/components/empty_state_spec.js
index c0e71e814d0..fbeaa32b1ec 100644
--- a/spec/frontend/groups/components/empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_state_spec.js
@@ -68,7 +68,7 @@ describe('EmptyState', () => {
it('renders empty state', () => {
createComponent({ provide: { canCreateSubgroups: false, canCreateProjects: false } });
- expect(wrapper.find(GlEmptyState).props()).toMatchObject({
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: EmptyState.i18n.withoutLinks.title,
description: EmptyState.i18n.withoutLinks.description,
svgPath: defaultProvide.emptySubgroupIllustration,
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 9906f62878f..3aa66644c19 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -8,9 +8,9 @@ import { getGroupItemMicrodata } from '~/groups/store/utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import { ITEM_TYPE } from '~/groups/constants';
import {
- VISIBILITY_LEVEL_PRIVATE,
- VISIBILITY_LEVEL_INTERNAL,
- VISIBILITY_LEVEL_PUBLIC,
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ VISIBILITY_LEVEL_INTERNAL_STRING,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
} from '~/visibility_level/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -19,7 +19,7 @@ import { mockParentGroupItem, mockChildren } from '../mock_data';
const createComponent = (
propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] },
provide = {
- currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE,
+ currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE_STRING,
},
) => {
return mountExtended(GroupItem, {
@@ -274,7 +274,7 @@ describe('GroupItemComponent', () => {
${'itemscope'} | ${'itemscope'}
${'itemtype'} | ${'https://schema.org/Organization'}
${'itemprop'} | ${'subOrganization'}
- `('it does set correct $attr', ({ attr, value } = {}) => {
+ `('does set correct $attr', ({ attr, value } = {}) => {
expect(wrapper.attributes(attr)).toBe(value);
});
@@ -283,7 +283,7 @@ describe('GroupItemComponent', () => {
${'img'} | ${'logo'}
${'[data-testid="group-name"]'} | ${'name'}
${'[data-testid="group-description"]'} | ${'description'}
- `('it does set correct $selector', ({ selector, propValue } = {}) => {
+ `('does set correct $selector', ({ selector, propValue } = {}) => {
expect(wrapper.find(selector).attributes('itemprop')).toBe(propValue);
});
});
@@ -320,16 +320,16 @@ describe('GroupItemComponent', () => {
describe('when showing projects', () => {
describe.each`
- itemVisibility | currentGroupVisibility | isPopoverShown
- ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PUBLIC} | ${false}
- ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PUBLIC} | ${false}
- ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PUBLIC} | ${false}
- ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PRIVATE} | ${false}
- ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PRIVATE} | ${true}
- ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PRIVATE} | ${true}
- ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_INTERNAL} | ${false}
- ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_INTERNAL} | ${false}
- ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_INTERNAL} | ${true}
+ itemVisibility | currentGroupVisibility | isPopoverShown
+ ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false}
+ ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false}
+ ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${true}
+ ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${true}
+ ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${false}
+ ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${true}
`(
'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility',
({ itemVisibility, currentGroupVisibility, isPopoverShown }) => {
@@ -374,7 +374,7 @@ describe('GroupItemComponent', () => {
wrapper = createComponent({
group: {
...mockParentGroupItem,
- visibility: VISIBILITY_LEVEL_PUBLIC,
+ visibility: VISIBILITY_LEVEL_PUBLIC_STRING,
type: ITEM_TYPE.PROJECT,
},
parentGroup: mockChildren[0],
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 6c1eb373b7e..866868eff36 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -6,7 +6,7 @@ import GroupItemComponent from '~/groups/components/group_item.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import GroupsComponent from '~/groups/components/groups.vue';
import eventHub from '~/groups/event_hub';
-import { VISIBILITY_LEVEL_PRIVATE } from '~/visibility_level/constants';
+import { VISIBILITY_LEVEL_PRIVATE_STRING } from '~/visibility_level/constants';
import { mockGroups, mockPageInfo } from '../mock_data';
describe('GroupsComponent', () => {
@@ -26,7 +26,7 @@ describe('GroupsComponent', () => {
...propsData,
},
provide: {
- currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE,
+ currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE_STRING,
},
});
};
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index 1924f400861..d25b45bd662 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -71,7 +71,7 @@ describe('InviteMembersBanner', () => {
describe('when the button is clicked', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- wrapper.find(GlBanner).vm.$emit('primary');
+ wrapper.findComponent(GlBanner).vm.$emit('primary');
});
it('calls openModal through the eventHub', () => {
@@ -92,7 +92,7 @@ describe('InviteMembersBanner', () => {
mockAxios.onPost(provide.calloutsPath).replyOnce(200);
const dismissEvent = 'invite_members_banner_dismissed';
- wrapper.find(GlBanner).vm.$emit('close');
+ wrapper.findComponent(GlBanner).vm.$emit('close');
expect(trackingSpy).toHaveBeenCalledWith(trackCategory, dismissEvent, {
label: provide.trackLabel,
@@ -102,7 +102,7 @@ describe('InviteMembersBanner', () => {
describe('rendering', () => {
const findBanner = () => {
- return wrapper.find(GlBanner);
+ return wrapper.findComponent(GlBanner);
};
beforeEach(() => {
@@ -132,16 +132,16 @@ describe('InviteMembersBanner', () => {
});
it('should render the banner when not dismissed', () => {
- expect(wrapper.find(GlBanner).exists()).toBe(true);
+ expect(wrapper.findComponent(GlBanner).exists()).toBe(true);
});
it('should close the banner when dismiss is clicked', async () => {
mockAxios.onPost(provide.calloutsPath).replyOnce(200);
- expect(wrapper.find(GlBanner).exists()).toBe(true);
- wrapper.find(GlBanner).vm.$emit('close');
+ expect(wrapper.findComponent(GlBanner).exists()).toBe(true);
+ wrapper.findComponent(GlBanner).vm.$emit('close');
await nextTick();
- expect(wrapper.find(GlBanner).exists()).toBe(false);
+ expect(wrapper.findComponent(GlBanner).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js
index 4bf92bb5642..2333f04bb2e 100644
--- a/spec/frontend/groups/components/item_caret_spec.js
+++ b/spec/frontend/groups/components/item_caret_spec.js
@@ -22,8 +22,8 @@ describe('ItemCaret', () => {
}
});
- const findAllGlIcons = () => wrapper.findAll(GlIcon);
- const findGlIcon = () => wrapper.find(GlIcon);
+ const findAllGlIcons = () => wrapper.findAllComponents(GlIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
describe('template', () => {
it('renders component template correctly', () => {
diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js
index fdc267bc14a..0c2912adc66 100644
--- a/spec/frontend/groups/components/item_stats_spec.js
+++ b/spec/frontend/groups/components/item_stats_spec.js
@@ -24,7 +24,7 @@ describe('ItemStats', () => {
}
});
- const findItemStatsValue = () => wrapper.find(ItemStatsValue);
+ const findItemStatsValue = () => wrapper.findComponent(ItemStatsValue);
describe('template', () => {
it('renders component container element correctly', () => {
diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js
index 98186120a81..b9db83c7dd7 100644
--- a/spec/frontend/groups/components/item_stats_value_spec.js
+++ b/spec/frontend/groups/components/item_stats_value_spec.js
@@ -25,7 +25,7 @@ describe('ItemStatsValue', () => {
}
});
- const findGlIcon = () => wrapper.find(GlIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
const findStatValue = () => wrapper.find('[data-testid="itemStatValue"]');
describe('template', () => {
diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js
index f3652f1a410..aa00e82150b 100644
--- a/spec/frontend/groups/components/item_type_icon_spec.js
+++ b/spec/frontend/groups/components/item_type_icon_spec.js
@@ -23,7 +23,7 @@ describe('ItemTypeIcon', () => {
}
});
- const findGlIcon = () => wrapper.find(GlIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
describe('template', () => {
it('renders component template correctly', () => {
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
new file mode 100644
index 00000000000..352bf25b84f
--- /dev/null
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -0,0 +1,187 @@
+import { GlTab } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import OverviewTabs from '~/groups/components/overview_tabs.vue';
+import GroupsApp from '~/groups/components/app.vue';
+import GroupsStore from '~/groups/store/groups_store';
+import GroupsService from '~/groups/service/groups_service';
+import { createRouter } from '~/groups/init_overview_tabs';
+import {
+ ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ ACTIVE_TAB_SHARED,
+ ACTIVE_TAB_ARCHIVED,
+} from '~/groups/constants';
+import axios from '~/lib/utils/axios_utils';
+
+const router = createRouter();
+
+describe('OverviewTabs', () => {
+ let wrapper;
+
+ const endpoints = {
+ subgroups_and_projects: '/groups/foobar/-/children.json',
+ shared: '/groups/foobar/-/shared_projects.json',
+ archived: '/groups/foobar/-/children.json?archived=only',
+ };
+
+ const routerMock = {
+ push: jest.fn(),
+ };
+
+ const createComponent = async ({
+ route = { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } },
+ } = {}) => {
+ wrapper = mountExtended(OverviewTabs, {
+ router,
+ provide: {
+ endpoints,
+ },
+ mocks: { $route: route, $router: routerMock },
+ });
+
+ await nextTick();
+ };
+
+ const findTabPanels = () => wrapper.findAllComponents(GlTab);
+ const findTab = (name) => wrapper.findByRole('tab', { name });
+ const findSelectedTab = () => wrapper.findByRole('tab', { selected: true });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(async () => {
+ // eslint-disable-next-line no-new
+ new AxiosMockAdapter(axios);
+ });
+
+ it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
+ await createComponent();
+
+ const tabPanel = findTabPanels().at(0);
+
+ expect(tabPanel.vm.$attrs).toMatchObject({
+ title: OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ lazy: false,
+ });
+ expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
+ action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ store: new GroupsStore({ showSchemaMarkup: true }),
+ service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ hideProjects: false,
+ renderEmptyState: true,
+ });
+ });
+
+ it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => {
+ await createComponent();
+
+ const tabPanel = findTabPanels().at(1);
+
+ expect(tabPanel.vm.$attrs).toMatchObject({
+ title: OverviewTabs.i18n[ACTIVE_TAB_SHARED],
+ lazy: true,
+ });
+
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_SHARED]).trigger('click');
+
+ expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
+ action: ACTIVE_TAB_SHARED,
+ store: new GroupsStore(),
+ service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
+ hideProjects: false,
+ renderEmptyState: false,
+ });
+
+ expect(tabPanel.vm.$attrs.lazy).toBe(false);
+ });
+
+ it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => {
+ await createComponent();
+
+ const tabPanel = findTabPanels().at(2);
+
+ expect(tabPanel.vm.$attrs).toMatchObject({
+ title: OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED],
+ lazy: true,
+ });
+
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+
+ expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
+ action: ACTIVE_TAB_ARCHIVED,
+ store: new GroupsStore(),
+ service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
+ hideProjects: false,
+ renderEmptyState: false,
+ });
+
+ expect(tabPanel.vm.$attrs.lazy).toBe(false);
+ });
+
+ describe.each([
+ [
+ { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } },
+ OverviewTabs.i18n[ACTIVE_TAB_SHARED],
+ {
+ name: ACTIVE_TAB_SHARED,
+ params: { group: ['foo', 'bar', 'baz'] },
+ },
+ ],
+ [
+ { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: ['foo', 'bar', 'baz'] } },
+ OverviewTabs.i18n[ACTIVE_TAB_SHARED],
+ {
+ name: ACTIVE_TAB_SHARED,
+ params: { group: ['foo', 'bar', 'baz'] },
+ },
+ ],
+ [
+ { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo' } },
+ OverviewTabs.i18n[ACTIVE_TAB_SHARED],
+ {
+ name: ACTIVE_TAB_SHARED,
+ params: { group: ['foo'] },
+ },
+ ],
+ [
+ { name: ACTIVE_TAB_SHARED, params: { group: 'foo/bar' } },
+ OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED],
+ {
+ name: ACTIVE_TAB_ARCHIVED,
+ params: { group: ['foo', 'bar'] },
+ },
+ ],
+ [
+ { name: ACTIVE_TAB_SHARED, params: { group: 'foo/bar' } },
+ OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: ['foo', 'bar'] },
+ },
+ ],
+ [
+ { name: ACTIVE_TAB_ARCHIVED, params: { group: ['foo'] } },
+ OverviewTabs.i18n[ACTIVE_TAB_SHARED],
+ {
+ name: ACTIVE_TAB_SHARED,
+ params: { group: ['foo'] },
+ },
+ ],
+ ])('when current route is %j', (currentRoute, tabToClick, expectedRoute) => {
+ beforeEach(async () => {
+ await createComponent({ route: currentRoute });
+ });
+
+ it(`sets ${OverviewTabs.i18n[currentRoute.name]} as active tab`, () => {
+ expect(findSelectedTab().text()).toBe(OverviewTabs.i18n[currentRoute.name]);
+ });
+
+ it(`pushes expected route when ${tabToClick} tab is clicked`, async () => {
+ await findTab(tabToClick).trigger('click');
+
+ expect(routerMock.push).toHaveBeenCalledWith(expectedRoute);
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/visibility_level_dropdown_spec.js b/spec/frontend/groups/components/visibility_level_dropdown_spec.js
deleted file mode 100644
index 61b7bbb0833..00000000000
--- a/spec/frontend/groups/components/visibility_level_dropdown_spec.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Component from '~/groups/components/visibility_level_dropdown.vue';
-
-describe('Visibility Level Dropdown', () => {
- let wrapper;
-
- const options = [
- { level: 0, label: 'Private', description: 'Private description' },
- { level: 20, label: 'Public', description: 'Public description' },
- ];
- const defaultLevel = 0;
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(Component, {
- propsData,
- });
- };
-
- beforeEach(() => {
- createComponent({
- visibilityLevelOptions: options,
- defaultLevel,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const hiddenInputValue = () =>
- wrapper.find("input[name='group[visibility_level]']").attributes('value');
- const dropdownText = () => wrapper.find(GlDropdown).props('text');
- const findDropdownItems = () =>
- wrapper.findAll(GlDropdownItem).wrappers.map((option) => ({
- text: option.text(),
- secondaryText: option.props('secondaryText'),
- }));
-
- describe('Default values', () => {
- it('sets the value of the hidden input to the default value', () => {
- expect(hiddenInputValue()).toBe(options[0].level.toString());
- });
-
- it('sets the text of the dropdown to the default value', () => {
- expect(dropdownText()).toBe(options[0].label);
- });
-
- it('shows all dropdown options', () => {
- expect(findDropdownItems()).toEqual(
- options.map(({ label, description }) => ({ text: label, secondaryText: description })),
- );
- });
- });
-
- describe('Selecting an option', () => {
- beforeEach(() => {
- wrapper.findAll(GlDropdownItem).at(1).vm.$emit('click');
- });
-
- it('sets the value of the hidden input to the selected value', () => {
- expect(hiddenInputValue()).toBe(options[1].level.toString());
- });
-
- it('sets the text of the dropdown to the selected value', () => {
- expect(dropdownText()).toBe(options[1].label);
- });
- });
-});
diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js
index 9515ca8c812..40c1843d461 100644
--- a/spec/frontend/header_search/init_spec.js
+++ b/spec/frontend/header_search/init_spec.js
@@ -24,10 +24,10 @@ describe('Header Search EventListener', () => {
const addEventListenerSpy = jest.spyOn(searchInputBox, 'addEventListener');
initHeaderSearch();
- expect(addEventListenerSpy).toBeCalledTimes(2);
+ expect(addEventListenerSpy).toHaveBeenCalledTimes(2);
});
- it('removes event listener ', async () => {
+ it('removes event listener', async () => {
const searchInputBox = document?.querySelector('#search');
const removeEventListenerSpy = jest.spyOn(searchInputBox, 'removeEventListener');
jest.mock('~/header_search', () => ({ initHeaderSearchApp: jest.fn() }));
@@ -39,7 +39,7 @@ describe('Header Search EventListener', () => {
[cleanEventListeners],
);
- expect(removeEventListenerSpy).toBeCalledTimes(2);
+ expect(removeEventListenerSpy).toHaveBeenCalledTimes(2);
});
it('attaches new vue dropdown when feature flag is enabled', async () => {
@@ -53,7 +53,7 @@ describe('Header Search EventListener', () => {
() => {},
);
- expect(mockVueApp).toBeCalled();
+ expect(mockVueApp).toHaveBeenCalled();
});
it('attaches old vue dropdown when feature flag is disabled', async () => {
@@ -69,6 +69,6 @@ describe('Header Search EventListener', () => {
() => {},
);
- expect(mockLegacyApp).toBeCalled();
+ expect(mockLegacyApp).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
index 8ccd7fb17e3..3a8624ad9dd 100644
--- a/spec/frontend/header_search/mock_data.js
+++ b/spec/frontend/header_search/mock_data.js
@@ -223,6 +223,20 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [
export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
{
+ category: 'Groups',
+ data: [
+ {
+ category: 'Groups',
+ html_id: 'autocomplete-Groups-1',
+
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ url: 'group/1',
+ },
+ ],
+ },
+ {
category: 'Projects',
data: [
{
@@ -246,20 +260,6 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
],
},
{
- category: 'Groups',
- data: [
- {
- category: 'Groups',
- html_id: 'autocomplete-Groups-1',
-
- id: 1,
- label: 'Gitlab Org / MockGroup1',
- value: 'MockGroup1',
- url: 'group/1',
- },
- ],
- },
- {
category: 'Help',
data: [
{
@@ -275,6 +275,14 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
{
+ category: 'Groups',
+ html_id: 'autocomplete-Groups-1',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ url: 'group/1',
+ },
+ {
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
@@ -291,14 +299,6 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
url: 'project/2',
},
{
- category: 'Groups',
- html_id: 'autocomplete-Groups-1',
- id: 1,
- label: 'Gitlab Org / MockGroup1',
- value: 'MockGroup1',
- url: 'group/1',
- },
- {
category: 'Help',
html_id: 'autocomplete-Help-3',
label: 'GitLab Help',
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
index 1748d89a6d3..1ae149128ca 100644
--- a/spec/frontend/header_search/store/actions_spec.js
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -2,9 +2,18 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/header_search/store/actions';
import * as types from '~/header_search/store/mutation_types';
-import createState from '~/header_search/store/state';
+import initState from '~/header_search/store/state';
import axios from '~/lib/utils/axios_utils';
-import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS_RES } from '../mock_data';
+import {
+ MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS_RES,
+ MOCK_AUTOCOMPLETE_PATH,
+ MOCK_PROJECT,
+ MOCK_SEARCH_CONTEXT,
+ MOCK_SEARCH_PATH,
+ MOCK_MR_PATH,
+ MOCK_ISSUE_PATH,
+} from '../mock_data';
jest.mock('~/flash');
@@ -12,10 +21,15 @@ describe('Header Search Store Actions', () => {
let state;
let mock;
- beforeEach(() => {
- state = createState({});
- mock = new MockAdapter(axios);
- });
+ const createState = (initialState) =>
+ initState({
+ searchPath: MOCK_SEARCH_PATH,
+ issuesPath: MOCK_ISSUE_PATH,
+ mrPath: MOCK_MR_PATH,
+ autocompletePath: MOCK_AUTOCOMPLETE_PATH,
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ });
afterEach(() => {
state = null;
@@ -24,12 +38,14 @@ describe('Header Search Store Actions', () => {
describe.each`
axiosMock | type | expectedMutations
- ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
- ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
+ ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
+ ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
`('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => {
describe(`on ${type}`, () => {
beforeEach(() => {
- mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
+ state = createState({});
+ mock = new MockAdapter(axios);
+ mock[axiosMock.method]().reply(axiosMock.code, axiosMock.res);
});
it(`should dispatch the correct mutations`, () => {
return testAction({
@@ -41,7 +57,35 @@ describe('Header Search Store Actions', () => {
});
});
+ describe.each`
+ project | ref | fetchType | expectedPath
+ ${null} | ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}`}
+ ${MOCK_PROJECT} | ${null} | ${'generic'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&filter=generic`}
+ ${null} | ${MOCK_PROJECT.id} | ${'generic'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_ref=${MOCK_PROJECT.id}&filter=generic`}
+ ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${'search'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}&filter=search`}
+ `('autocompleteQuery', ({ project, ref, fetchType, expectedPath }) => {
+ describe(`when project is ${project?.name} and project ref is ${ref}`, () => {
+ beforeEach(() => {
+ state = createState({
+ search: MOCK_SEARCH,
+ searchContext: {
+ project,
+ ref,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(actions.autocompleteQuery({ state, fetchType })).toBe(expectedPath);
+ });
+ });
+ });
+
describe('clearAutocomplete', () => {
+ beforeEach(() => {
+ state = createState({});
+ });
+
it('calls the CLEAR_AUTOCOMPLETE mutation', () => {
return testAction({
action: actions.clearAutocomplete,
@@ -52,6 +96,10 @@ describe('Header Search Store Actions', () => {
});
describe('setSearch', () => {
+ beforeEach(() => {
+ state = createState({});
+ });
+
it('calls the SET_SEARCH mutation', () => {
return testAction({
action: actions.setSearch,
diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js
index c76be3c0360..a1d9481b5cc 100644
--- a/spec/frontend/header_search/store/getters_spec.js
+++ b/spec/frontend/header_search/store/getters_spec.js
@@ -73,30 +73,6 @@ describe('Header Search Store Getters', () => {
});
describe.each`
- project | ref | expectedPath
- ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}`}
- ${MOCK_PROJECT} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}`}
- ${null} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_ref=${MOCK_PROJECT.id}`}
- ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}`}
- `('autocompleteQuery', ({ project, ref, expectedPath }) => {
- describe(`when project is ${project?.name} and project ref is ${ref}`, () => {
- beforeEach(() => {
- createState({
- searchContext: {
- project,
- ref,
- },
- });
- state.search = MOCK_SEARCH;
- });
-
- it(`should return ${expectedPath}`, () => {
- expect(getters.autocompleteQuery(state)).toBe(expectedPath);
- });
- });
- });
-
- describe.each`
group | group_metadata | project | project_metadata | expectedPath
${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH}
${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index 81c81fc0a9f..4406d14d990 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -40,7 +40,7 @@ describe('Multi-file editor commit sidebar list', () => {
wrapper = mountComponent({ fileList: [] });
});
- it('renders no changes text ', () => {
+ it('renders no changes text', () => {
expect(wrapper.text()).toContain('No changes');
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
index d899bc4f7d8..ee6ed694285 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
+import RadioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
import { createStore } from '~/ide/stores';
describe('IDE commit sidebar radio group', () => {
@@ -10,7 +10,7 @@ describe('IDE commit sidebar radio group', () => {
beforeEach(async () => {
store = createStore();
- const Component = Vue.extend(radioGroup);
+ const Component = Vue.extend(RadioGroup);
store.state.commit.commitAction = '2';
@@ -38,7 +38,7 @@ describe('IDE commit sidebar radio group', () => {
vm = new Vue({
components: {
- radioGroup,
+ RadioGroup,
},
store,
render: (createElement) =>
@@ -62,7 +62,7 @@ describe('IDE commit sidebar radio group', () => {
beforeEach(async () => {
vm.$destroy();
- const Component = Vue.extend(radioGroup);
+ const Component = Vue.extend(RadioGroup);
store.state.commit.commitAction = '1';
store.state.commit.newBranchName = 'test-123';
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
index 532cb6e795c..043dcade858 100644
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ b/spec/frontend/ide/components/preview/navigator_spec.js
@@ -76,7 +76,7 @@ describe('IDE clientside preview navigator', () => {
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
await nextTick();
findBackButton().trigger('click');
- expect(findBackButton().attributes('disabled')).toBeFalsy();
+ expect(findBackButton().attributes()).not.toHaveProperty('disabled');
});
it('is disabled when there is no previous entry', async () => {
@@ -117,7 +117,7 @@ describe('IDE clientside preview navigator', () => {
findBackButton().trigger('click');
await nextTick();
- expect(findForwardButton().attributes('disabled')).toBeFalsy();
+ expect(findForwardButton().attributes()).not.toHaveProperty('disabled');
});
it('is disabled when there is no next entry', async () => {
diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js
index a37c08af0a1..2efef9918b1 100644
--- a/spec/frontend/ide/components/shared/tokened_input_spec.js
+++ b/spec/frontend/ide/components/shared/tokened_input_spec.js
@@ -50,7 +50,7 @@ describe('IDE shared/TokenedInput', () => {
});
it('renders input', () => {
- expect(vm.$refs.input).toBeTruthy();
+ expect(vm.$refs.input).toBeInstanceOf(HTMLInputElement);
expect(vm.$refs.input).toHaveValue(TEST_VALUE);
});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
new file mode 100644
index 00000000000..ec8559f1b56
--- /dev/null
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -0,0 +1,62 @@
+import { start } from '@gitlab/web-ide';
+import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
+import { TEST_HOST } from 'helpers/test_constants';
+
+jest.mock('@gitlab/web-ide');
+
+const ROOT_ELEMENT_ID = 'ide';
+const TEST_NONCE = 'test123nonce';
+const TEST_PROJECT = { path_with_namespace: 'group1/project1' };
+const TEST_BRANCH_NAME = '12345-foo-patch';
+const TEST_GITLAB_URL = 'https://test-gitlab/';
+const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
+
+describe('ide/init_gitlab_web_ide', () => {
+ const createRootElement = () => {
+ const el = document.createElement('div');
+
+ el.id = ROOT_ELEMENT_ID;
+ // why: We'll test that this class is removed later
+ el.classList.add('ide-loading');
+ el.dataset.project = JSON.stringify(TEST_PROJECT);
+ el.dataset.cspNonce = TEST_NONCE;
+ el.dataset.branchName = TEST_BRANCH_NAME;
+
+ document.body.append(el);
+ };
+ const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID);
+ const act = () => initGitlabWebIDE(findRootElement());
+
+ beforeEach(() => {
+ process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH;
+ window.gon.gitlab_url = TEST_GITLAB_URL;
+
+ createRootElement();
+
+ act();
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('calls start with element', () => {
+ expect(start).toHaveBeenCalledWith(findRootElement(), {
+ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ projectPath: TEST_PROJECT.path_with_namespace,
+ ref: TEST_BRANCH_NAME,
+ gitlabUrl: TEST_GITLAB_URL,
+ nonce: TEST_NONCE,
+ });
+ });
+
+ it('clears classes and data from root element', () => {
+ const rootEl = findRootElement();
+
+ // why: Snapshot to test that `ide-loading` was removed and no other
+ // artifacts are remaining.
+ expect(rootEl.outerHTML).toBe(
+ '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>',
+ );
+ });
+});
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index d1c31cd412b..38a54e569a9 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -78,7 +78,7 @@ describe('IDE store file actions', () => {
});
});
- it('switches to the next available file before closing the current one ', () => {
+ it('switches to the next available file before closing the current one', () => {
const f = file('newOpenFile');
store.state.openFiles.push(f);
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index d65039e89cc..4e8467de759 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -210,7 +210,7 @@ describe('IDE commit module actions', () => {
branch,
});
store.state.openFiles.forEach((entry) => {
- expect(entry.changed).toBeFalsy();
+ expect(entry.changed).toBe(false);
});
});
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index 1c1e1e7ebd4..b896437ecb2 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -42,7 +42,7 @@ describe('Import entities group dropdown component', () => {
createComponent({ namespaces });
namespacesTracker.mockReset();
- wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
await nextTick();
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
index cdc508a0033..f97ea046cbe 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -99,7 +99,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not renders loading icon when request is completed', async () => {
@@ -108,7 +108,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
});
@@ -123,7 +123,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
+ expect(wrapper.findComponent(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
});
});
@@ -268,7 +268,7 @@ describe('import table', () => {
});
it('correctly passes pagination info from query', () => {
- expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
+ expect(wrapper.findComponent(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
});
it('renders pagination dropdown', () => {
@@ -293,7 +293,7 @@ describe('import table', () => {
it('updates page when page change is requested', async () => {
const REQUESTED_PAGE = 2;
- wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+ wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@@ -316,7 +316,7 @@ describe('import table', () => {
},
versionValidation: FAKE_VERSION_VALIDATION,
});
- wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+ wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from');
@@ -539,8 +539,8 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlAlert).exists()).toBe(true);
- expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)');
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
});
it('does not renders alert when there are no unavailable features', async () => {
@@ -558,7 +558,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index d3f86672f33..18dc1217fec 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -22,8 +22,8 @@ describe('import target cell', () => {
let wrapper;
let group;
- const findNameInput = () => wrapper.find(GlFormInput);
- const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
+ const findNameInput = () => wrapper.findComponent(GlFormInput);
+ const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const createComponent = (props) => {
wrapper = shallowMount(ImportTargetCell, {
diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
index ea88c361f7b..9eae4ed974e 100644
--- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
@@ -33,12 +33,12 @@ describe('BitbucketStatusTable', () => {
it('renders import table component', () => {
createComponent({ providerTitle: 'Test' });
- expect(wrapper.find(ImportProjectsTable).exists()).toBe(true);
+ expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true);
});
it('passes alert in incompatible-repos-warning slot', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
it('passes actions slot to import project table component', () => {
@@ -46,14 +46,14 @@ describe('BitbucketStatusTable', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
actions: actionsSlotContent,
});
- expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent);
+ expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
});
it('dismisses alert when requested', async () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
- wrapper.find(GlAlert).vm.$emit('dismiss');
+ wrapper.findComponent(GlAlert).vm.$emit('dismiss');
await nextTick();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 140fec3863b..c0ae4294e3d 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -30,10 +30,10 @@ describe('ImportProjectsTable', () => {
const findImportAllButton = () =>
wrapper
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.filter((w) => w.props().variant === 'confirm')
.at(0);
- const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
+ const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
@@ -89,13 +89,13 @@ describe('ImportProjectsTable', () => {
it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders a table with provider repos', () => {
@@ -109,7 +109,7 @@ describe('ImportProjectsTable', () => {
state: { namespaces: [{ fullPath: 'path' }], repositories },
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('table').exists()).toBe(true);
expect(
wrapper
@@ -118,7 +118,7 @@ describe('ImportProjectsTable', () => {
.exists(),
).toBe(true);
- expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length);
+ expect(wrapper.findAllComponents(ProviderRepoTableRow)).toHaveLength(repositories.length);
});
it.each`
@@ -170,7 +170,7 @@ describe('ImportProjectsTable', () => {
it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } });
- expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
+ expect(wrapper.findComponent(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
@@ -231,11 +231,11 @@ describe('ImportProjectsTable', () => {
});
it('renders intersection observer component', () => {
- expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
});
it('calls fetchRepos when intersection observer appears', async () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 41a005199e1..17a07b1e9f9 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -74,11 +74,13 @@ describe('ProviderRepoTableRow', () => {
});
it('renders empty import status', () => {
- expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
+ expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE);
});
it('renders a group namespace select', () => {
- expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces);
+ expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe(
+ availableNamespaces,
+ );
});
it('renders import button', () => {
@@ -127,11 +129,13 @@ describe('ProviderRepoTableRow', () => {
});
it('renders proper import status', () => {
- expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
+ expect(wrapper.findComponent(ImportStatus).props().status).toBe(
+ repo.importedProject.importStatus,
+ );
});
it('does not renders a namespace select', () => {
- expect(wrapper.find(GlDropdown).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});
it('does not render import button', () => {
@@ -139,7 +143,7 @@ describe('ProviderRepoTableRow', () => {
});
it('passes stats to import status component', () => {
- expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS);
+ expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
});
});
@@ -165,7 +169,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders badge with error', () => {
- expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
+ expect(wrapper.findComponent(GlBadge).text()).toBe('Incompatible project');
});
});
});
diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js
index 55826b20ca3..110b692b222 100644
--- a/spec/frontend/import_entities/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js
@@ -85,7 +85,7 @@ describe('import_projects store getters', () => {
});
describe('hasImportableRepos', () => {
- it('returns true if there are any importable projects ', () => {
+ it('returns true if there are any importable projects', () => {
localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
expect(hasImportableRepos(localState)).toBe(true);
@@ -99,7 +99,7 @@ describe('import_projects store getters', () => {
});
describe('importAllCount', () => {
- it('returns count of available importable projects ', () => {
+ it('returns count of available importable projects', () => {
localState.repositories = [
IMPORTABLE_REPO,
IMPORTABLE_REPO,
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 356480f931e..e8d222dc2e9 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -40,16 +40,16 @@ describe('Incidents List', () => {
all: 26,
};
- const findTable = () => wrapper.find(GlTable);
+ const findTable = () => wrapper.findComponent(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr');
- const findAlert = () => wrapper.find(GlAlert);
- const findLoader = () => wrapper.find(GlLoadingIcon);
- const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
+ const findTimeAgo = () => wrapper.findAllComponents(TimeAgoTooltip);
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
- const findEmptyState = () => wrapper.find(GlEmptyState);
- const findSeverity = () => wrapper.findAll(SeverityToken);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findSeverity = () => wrapper.findAllComponents(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
const findIncidentLink = () => wrapper.findByTestId('incident-link');
@@ -179,7 +179,7 @@ describe('Incidents List', () => {
});
it('renders an avatar component when there is an assignee', () => {
- const avatar = findAssignees().at(1).find(GlAvatar);
+ const avatar = findAssignees().at(1).findComponent(GlAvatar);
const { src, label } = avatar.attributes();
const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
index ff40f1fa008..394d1f12bcb 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
@@ -20,10 +20,10 @@ describe('IncidentsSettingTabs', () => {
}
});
- const findToggleButton = () => wrapper.find({ ref: 'toggleBtn' });
- const findSectionHeader = () => wrapper.find({ ref: 'sectionHeader' });
+ const findToggleButton = () => wrapper.findComponent({ ref: 'toggleBtn' });
+ const findSectionHeader = () => wrapper.findComponent({ ref: 'sectionHeader' });
- const findIntegrationTabs = () => wrapper.findAll(GlTab);
+ const findIntegrationTabs = () => wrapper.findAllComponents(GlTab);
it('renders header text', () => {
expect(findSectionHeader().text()).toBe('Incidents');
});
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 8ee55928926..c329ca8522f 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -24,7 +24,7 @@ describe('TriggerFields', () => {
});
const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label');
- const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAll(GlFormGroup);
+ const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAllComponents(GlFormGroup);
const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
const findAllGlFormInputs = () => wrapper.findAllComponents(GlFormInput);
@@ -86,7 +86,7 @@ describe('TriggerFields', () => {
expect(checkboxes).toHaveLength(2);
checkboxes.wrappers.forEach((checkbox, index) => {
- const checkBox = checkbox.find(GlFormCheckbox);
+ const checkBox = checkbox.findComponent(GlFormCheckbox);
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
expect(checkbox.find('[type=hidden]').attributes('name')).toBe(
diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
index b4d42d90d99..8b2d13be309 100644
--- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
@@ -53,7 +53,7 @@ afterEach(() => {
describe('ImportProjectMembersModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
- const findIntroText = () => wrapper.find({ ref: 'modalIntro' }).text();
+ const findIntroText = () => wrapper.findComponent({ ref: 'modalIntro' }).text();
const clickImportButton = () => findGlModal().vm.$emit('primary', { preventDefault: jest.fn() });
const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() });
const findFormGroup = () => wrapper.findByTestId('form-group');
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 2058784b033..e9e1fbad07b 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -19,6 +19,7 @@ import {
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
EXPANDED_ERRORS,
+ EMPTY_INVITES_ERROR_TEXT,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -255,6 +256,8 @@ describe('InviteMembersModal', () => {
it('tracks the submit for invite_members_for_task', async () => {
await setupComponentWithTasks();
+ await triggerMembersTokenSelect([user1]);
+
clickInviteButton();
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
@@ -265,6 +268,16 @@ describe('InviteMembersModal', () => {
INVITE_MEMBERS_FOR_TASK.submit,
);
});
+
+ it('does not track the submit for invite_members_for_task when invites have not been entered', async () => {
+ await setupComponentWithTasks();
+ clickInviteButton();
+
+ expect(ExperimentTracking).not.toHaveBeenCalledWith(
+ INVITE_MEMBERS_FOR_TASK.name,
+ expect.any,
+ );
+ });
});
});
@@ -380,6 +393,25 @@ describe('InviteMembersModal', () => {
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
const expectedSyntaxError = 'email contains an invalid email address';
+ describe('when no invites have been entered in the form and then some are entered', () => {
+ beforeEach(async () => {
+ createInviteMembersToGroupWrapper();
+ });
+
+ it('displays an error', async () => {
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(EMPTY_INVITES_ERROR_TEXT);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+
+ await triggerMembersTokenSelect([user1]);
+
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ });
+ });
+
describe('when inviting an existing user to group by user ID', () => {
const postData = {
user_id: '1,2',
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
index 543fc28a342..1ff2e86412f 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -1,12 +1,7 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
-
-import {
- REACHED_LIMIT_MESSAGE,
- REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
-} from '~/invite_members/constants';
-
+import { REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE } from '~/invite_members/constants';
import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name';
@@ -52,22 +47,6 @@ describe('UserLimitNotification', () => {
});
});
- describe('when close to limit within a personal namepace', () => {
- beforeEach(() => {
- createComponent(true, false, { membersCount: 3, userNamespace: true });
- });
-
- it('renders the limit for a personal namespace', () => {
- const alert = findAlert();
-
- expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE);
-
- expect(alert.text()).toEqual(
- 'To make more space, you can remove members who no longer need access.',
- );
- });
- });
-
describe('when close to limit within a group', () => {
it("renders user's limit notification", () => {
createComponent(true, false, { membersCount: 3 });
@@ -91,19 +70,5 @@ describe('UserLimitNotification', () => {
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name");
expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE);
});
-
- describe('when free user namespace', () => {
- it("renders user's limit notification", () => {
- createComponent(true, true, { userNamespace: true });
-
- const alert = findAlert();
-
- expect(alert.attributes('title')).toEqual(
- "You've reached your 5 members limit for your personal projects",
- );
-
- expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);
- });
- });
});
});
diff --git a/spec/frontend/issuable/components/issue_assignees_spec.js b/spec/frontend/issuable/components/issue_assignees_spec.js
index 713c8b1dfdd..9a33bfae240 100644
--- a/spec/frontend/issuable/components/issue_assignees_spec.js
+++ b/spec/frontend/issuable/components/issue_assignees_spec.js
@@ -27,7 +27,7 @@ describe('IssueAssigneesComponent', () => {
});
const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
- const findAvatars = () => wrapper.findAll(UserAvatarLink);
+ const findAvatars = () => wrapper.findAllComponents(UserAvatarLink);
const findOverflowCounter = () => wrapper.find('.avatar-counter');
it('returns default data props', () => {
diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js
index 9d67f602136..eac53c5f761 100644
--- a/spec/frontend/issuable/components/issue_milestone_spec.js
+++ b/spec/frontend/issuable/components/issue_milestone_spec.js
@@ -144,7 +144,7 @@ describe('IssueMilestoneComponent', () => {
});
it('renders milestone icon', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe('clock');
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
});
it('renders milestone title', () => {
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index d844f3394d5..5e67ea42b87 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -1,111 +1,200 @@
import $ from 'jquery';
+import Autosave from '~/autosave';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
+jest.mock('~/autosave');
+
+const createIssuable = (form) => {
+ return new IssuableForm(form);
+};
+
describe('IssuableForm', () => {
+ let $form;
let instance;
- const createIssuable = (form) => {
- instance = new IssuableForm(form);
- };
-
beforeEach(() => {
setHTMLFixture(`
<form>
<input name="[title]" />
+ <textarea name="[description]"></textarea>
</form>
`);
- createIssuable($('form'));
+ $form = $('form');
});
afterEach(() => {
resetHTMLFixture();
+ $form = null;
+ instance = null;
});
- describe('initAutosave', () => {
- it('creates autosave with the searchTerm included', () => {
- setWindowLocation('https://gitlab.test/foo?bar=true');
- const autosave = instance.initAutosave();
+ describe('autosave', () => {
+ let $title;
+ let $description;
+
+ beforeEach(() => {
+ $title = $form.find('input[name*="[title]"]');
+ $description = $form.find('textarea[name*="[description]"]');
+ });
- expect(autosave.key.includes('bar=true')).toBe(true);
+ afterEach(() => {
+ $title = null;
+ $description = null;
});
- it("creates autosave fields without the searchTerm if it's an issue new form", () => {
- setHTMLFixture(`
- <form data-new-issue-path="/issues/new">
- <input name="[title]" />
- </form>
- `);
- createIssuable($('form'));
+ describe('initAutosave', () => {
+ it('calls initAutosave', () => {
+ const initAutosave = jest.spyOn(IssuableForm.prototype, 'initAutosave');
+ createIssuable($form);
+
+ expect(initAutosave).toHaveBeenCalledTimes(1);
+ });
+
+ it('creates autosave with the searchTerm included', () => {
+ setWindowLocation('https://gitlab.test/foo?bar=true');
+ createIssuable($form);
+
+ expect(Autosave).toHaveBeenCalledWith(
+ $title,
+ ['/foo', 'bar=true', 'title'],
+ 'autosave//foo/bar=true=title',
+ );
+ expect(Autosave).toHaveBeenCalledWith(
+ $description,
+ ['/foo', 'bar=true', 'description'],
+ 'autosave//foo/bar=true=description',
+ );
+ });
+
+ it("creates autosave fields without the searchTerm if it's an issue new form", () => {
+ setWindowLocation('https://gitlab.test/issues/new?bar=true');
+ $form.attr('data-new-issue-path', '/issues/new');
+ createIssuable($form);
+
+ expect(Autosave).toHaveBeenCalledWith(
+ $title,
+ ['/issues/new', '', 'title'],
+ 'autosave//issues/new/bar=true=title',
+ );
+ expect(Autosave).toHaveBeenCalledWith(
+ $description,
+ ['/issues/new', '', 'description'],
+ 'autosave//issues/new/bar=true=description',
+ );
+ });
+
+ it.each([
+ {
+ id: 'confidential',
+ input: '<input type="checkbox" name="issue[confidential]"/>',
+ selector: 'input[name*="[confidential]"]',
+ },
+ {
+ id: 'due_date',
+ input: '<input type="text" name="issue[due_date]"/>',
+ selector: 'input[name*="[due_date]"]',
+ },
+ ])('creates $id autosave when $id input exist', ({ id, input, selector }) => {
+ $form.append(input);
+ const $input = $form.find(selector);
+ const totalAutosaveFormFields = $form.children().length;
+ createIssuable($form);
+
+ expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields);
+ expect(Autosave).toHaveBeenLastCalledWith($input, ['/', '', id], `autosave///=${id}`);
+ });
+ });
+
+ describe('resetAutosave', () => {
+ it('calls reset on title and description', () => {
+ instance = createIssuable($form);
+
+ instance.resetAutosave();
+
+ expect(instance.autosaves.get('title').reset).toHaveBeenCalledTimes(1);
+ expect(instance.autosaves.get('description').reset).toHaveBeenCalledTimes(1);
+ });
- setWindowLocation('https://gitlab.test/issues/new?bar=true');
+ it('resets autosave when submit', () => {
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ createIssuable($form);
- const autosave = instance.initAutosave();
+ $form.submit();
- expect(autosave.key.includes('bar=true')).toBe(false);
+ expect(resetAutosave).toHaveBeenCalledTimes(1);
+ });
+
+ it('resets autosave on elements with the .js-reset-autosave class', () => {
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ $form.append('<a class="js-reset-autosave">Cancel</a>');
+ createIssuable($form);
+
+ $form.find('.js-reset-autosave').trigger('click');
+
+ expect(resetAutosave).toHaveBeenCalledTimes(1);
+ });
+
+ it.each([
+ { id: 'confidential', input: '<input type="checkbox" name="issue[confidential]"/>' },
+ { id: 'due_date', input: '<input type="text" name="issue[due_date]"/>' },
+ ])('calls reset on autosave $id when $id input exist', ({ id, input }) => {
+ $form.append(input);
+ instance = createIssuable($form);
+ instance.resetAutosave();
+
+ expect(instance.autosaves.get(id).reset).toHaveBeenCalledTimes(1);
+ });
});
});
- describe('resetAutosave', () => {
- it('resets autosave on elements with the .js-reset-autosave class', () => {
- setHTMLFixture(`
- <form>
- <input name="[title]" />
- <textarea name="[description]"></textarea>
- <a class="js-reset-autosave">Cancel</a>
- </form>
- `);
- const $form = $('form');
- const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
- createIssuable($form);
-
- $form.find('.js-reset-autosave').trigger('click');
-
- expect(resetAutosave).toHaveBeenCalled();
+ describe('wip', () => {
+ beforeEach(() => {
+ instance = createIssuable($form);
});
- });
- describe('removeWip', () => {
- it.each`
- prefix
- ${'draFT: '}
- ${' [DRaft] '}
- ${'drAft:'}
- ${'[draFT]'}
- ${'(draft) '}
- ${' (DrafT)'}
- ${'draft: [draft] (draft)'}
- `('removes "$prefix" from the beginning of the title', ({ prefix }) => {
- instance.titleField.val(`${prefix}The Issuable's Title Value`);
-
- instance.removeWip();
-
- expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ describe('removeWip', () => {
+ it.each`
+ prefix
+ ${'draFT: '}
+ ${' [DRaft] '}
+ ${'drAft:'}
+ ${'[draFT]'}
+ ${'(draft) '}
+ ${' (DrafT)'}
+ ${'draft: [draft] (draft)'}
+ `('removes "$prefix" from the beginning of the title', ({ prefix }) => {
+ instance.titleField.val(`${prefix}The Issuable's Title Value`);
+
+ instance.removeWip();
+
+ expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ });
});
- });
- describe('addWip', () => {
- it("properly adds the work in progress prefix to the Issuable's title", () => {
- instance.titleField.val("The Issuable's Title Value");
+ describe('addWip', () => {
+ it("properly adds the work in progress prefix to the Issuable's title", () => {
+ instance.titleField.val("The Issuable's Title Value");
- instance.addWip();
+ instance.addWip();
- expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ });
});
- });
- describe('workInProgress', () => {
- it.each`
- title | expected
- ${'draFT: something is happening'} | ${true}
- ${'draft something is happening'} | ${false}
- ${'something is happening to drafts'} | ${false}
- ${'something is happening'} | ${false}
- `('returns $expected with "$title"', ({ title, expected }) => {
- instance.titleField.val(title);
-
- expect(instance.workInProgress()).toBe(expected);
+ describe('workInProgress', () => {
+ it.each`
+ title | expected
+ ${'draFT: something is happening'} | ${true}
+ ${'draft something is happening'} | ${false}
+ ${'something is happening to drafts'} | ${false}
+ ${'something is happening'} | ${false}
+ `('returns $expected with "$title"', ({ title, expected }) => {
+ instance.titleField.val(title);
+
+ expect(instance.workInProgress()).toBe(expected);
+ });
});
});
});
diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
index d6aeacfe07a..bacebbade7f 100644
--- a/spec/frontend/issuable/related_issues/components/issue_token_spec.js
+++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
@@ -31,11 +31,11 @@ describe('IssueToken', () => {
}
});
- const findLink = () => wrapper.find({ ref: 'link' });
- const findReference = () => wrapper.find({ ref: 'reference' });
+ const findLink = () => wrapper.findComponent({ ref: 'link' });
+ const findReference = () => wrapper.findComponent({ ref: 'reference' });
const findReferenceIcon = () => wrapper.find('[data-testid="referenceIcon"]');
const findRemoveBtn = () => wrapper.find('[data-testid="removeBtn"]');
- const findTitle = () => wrapper.find({ ref: 'title' });
+ const findTitle = () => wrapper.findComponent({ ref: 'title' });
describe('with reference supplied', () => {
beforeEach(() => {
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 772cc75a205..1b2935ce5d1 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -153,7 +153,7 @@ describe('RelatedIssuesBlock', () => {
});
it('sets `autoCompleteEpics` to false for add-issuable-form', () => {
- expect(wrapper.find(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
+ expect(wrapper.findComponent(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
});
});
@@ -227,7 +227,7 @@ describe('RelatedIssuesBlock', () => {
},
});
- const iconComponent = wrapper.find(GlIcon);
+ const iconComponent = wrapper.findComponent(GlIcon);
expect(iconComponent.exists()).toBe(true);
expect(iconComponent.props('name')).toBe(icon);
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
index fd623ad9a5f..9bb71ec3dcb 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
@@ -187,7 +187,9 @@ describe('RelatedIssuesList', () => {
});
it('shows due date', () => {
- expect(wrapper.find(IssueDueDate).find('.board-card-info-text').text()).toBe('Nov 22, 2010');
+ expect(wrapper.findComponent(IssueDueDate).find('.board-card-info-text').text()).toBe(
+ 'Nov 22, 2010',
+ );
});
});
});
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index cb7173c56a8..cc2ee84348a 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -106,7 +106,7 @@ describe('CreateMergeRequestDropdown', () => {
loading | hasClass
${true} | ${false}
${false} | ${true}
- `('it toggle loading spinner when loading is $loading', ({ loading, hasClass }) => {
+ `('toggle loading spinner when loading is $loading', ({ loading, hasClass }) => {
dropdown.setLoading(loading);
expect(document.querySelector('.js-spinner').classList.contains('gl-display-none')).toEqual(
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index c3f13ca6f9a..b0d3a63a8cf 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -21,7 +21,7 @@ describe('CE IssueCardTimeInfo component', () => {
};
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
- const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title');
+ const findMilestoneTitle = () => findMilestone().findComponent(GlLink).attributes('title');
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
@@ -56,8 +56,8 @@ describe('CE IssueCardTimeInfo component', () => {
const milestone = findMilestone();
expect(milestone.text()).toBe(issue.milestone.title);
- expect(milestone.find(GlIcon).props('name')).toBe('clock');
- expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
+ expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
+ expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath);
});
describe.each`
@@ -84,7 +84,7 @@ describe('CE IssueCardTimeInfo component', () => {
expect(dueDate.text()).toBe('Dec 12, 2020');
expect(dueDate.attributes('title')).toBe('Due date');
- expect(dueDate.find(GlIcon).props('name')).toBe('calendar');
+ expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar');
expect(dueDate.classes()).not.toContain('gl-text-red-500');
});
});
@@ -118,6 +118,6 @@ describe('CE IssueCardTimeInfo component', () => {
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate');
- expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
+ expect(timeEstimate.findComponent(GlIcon).props('name')).toBe('timer');
});
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index a39853fd29c..5133c02b190 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
+import { GlButton, GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -29,7 +29,6 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
-
import {
CREATED_DESC,
RELATIVE_POSITION,
@@ -58,6 +57,10 @@ import {
WORK_ITEM_TYPE_ENUM_TASK,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+
+import('~/issuable/bulk_update_sidebar');
+import('~/users_select');
jest.mock('@sentry/browser');
jest.mock('~/flash');
@@ -122,7 +125,6 @@ describe('CE IssuesListApp component', () => {
const findGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAt = (index) => findGlButtons().at(index);
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findGlLink = () => wrapper.findComponent(GlLink);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
@@ -430,8 +432,9 @@ describe('CE IssuesListApp component', () => {
});
});
- it('is not set from url params', () => {
- expect(findIssuableList().props('initialFilterValue')).toEqual([]);
+ it('is set from url params and removes search terms', () => {
+ const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM);
+ expect(findIssuableList().props('initialFilterValue')).toEqual(expected);
});
it('shows an alert to tell the user they must be signed in to search', () => {
@@ -562,15 +565,16 @@ describe('CE IssuesListApp component', () => {
it('shows Jira integration information', () => {
const paragraphs = wrapper.findAll('p');
- expect(paragraphs.at(2).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
- expect(paragraphs.at(3).text()).toContain(
+ const links = wrapper.findAll('.gl-link');
+ expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
+ expect(paragraphs.at(2).text()).toContain(
'Enable the Jira integration to view your Jira issues in GitLab.',
);
- expect(paragraphs.at(4).text()).toContain(
+ expect(paragraphs.at(3).text()).toContain(
IssuesListApp.i18n.jiraIntegrationSecondaryMessage,
);
- expect(findGlLink().text()).toBe('Enable the Jira integration');
- expect(findGlLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
+ expect(links.at(1).text()).toBe('Enable the Jira integration');
+ expect(links.at(1).attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
});
});
@@ -1006,8 +1010,9 @@ describe('CE IssuesListApp component', () => {
findIssuableList().vm.$emit('filter', filteredTokens);
});
- it('does not update url params', () => {
- expect(router.push).not.toHaveBeenCalled();
+ it('removes search terms', () => {
+ const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM);
+ expect(findIssuableList().props('initialFilterValue')).toEqual(expected);
});
it('shows an alert to tell the user they must be signed in to search', () => {
diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index 2d773e8bf56..406b1fbc1af 100644
--- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -11,9 +11,9 @@ describe('JiraIssuesImportStatus', () => {
};
let wrapper;
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
- const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
+ const findAlertLabel = () => wrapper.findComponent(GlAlert).findComponent(GlLabel);
const mountComponent = ({
shouldShowFinishedAlert = false,
@@ -49,7 +49,7 @@ describe('JiraIssuesImportStatus', () => {
});
it('does not show an alert', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
@@ -105,12 +105,12 @@ describe('JiraIssuesImportStatus', () => {
shouldShowInProgressAlert: true,
});
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/new/components/title_suggestions_item_spec.js b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
index 5eb30b52de5..c54a762440f 100644
--- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
@@ -20,7 +20,7 @@ describe('Issue title suggestions item component', () => {
}
const findLink = () => wrapper.findComponent(GlLink);
- const findAuthorLink = () => wrapper.findAll(GlLink).at(1);
+ const findAuthorLink = () => wrapper.findAllComponents(GlLink).at(1);
const findIcon = () => wrapper.findComponent(GlIcon);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findUserAvatar = () => wrapper.findComponent(UserAvatarImage);
@@ -105,7 +105,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1');
- expect(count.find(GlIcon).props('name')).toBe('thumb-up');
+ expect(count.findComponent(GlIcon).props('name')).toBe('thumb-up');
});
it('renders notes count', () => {
@@ -114,7 +114,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2');
- expect(count.find(GlIcon).props('name')).toBe('comment');
+ expect(count.findComponent(GlIcon).props('name')).toBe('comment');
});
});
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index 0a64890e4ca..1cd6576967a 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -83,7 +83,7 @@ describe('Issue title suggestions component', () => {
wrapper.setData(data);
await nextTick();
- expect(wrapper.findAll(TitleSuggestionsItem).length).toBe(2);
+ expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(2);
});
it('adds margin class to first item', async () => {
diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
index 4df04cd5257..d30a8c081cc 100644
--- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
@@ -65,9 +65,9 @@ describe('RelatedMergeRequests', () => {
describe('template', () => {
it('should render related merge request items', () => {
expect(wrapper.find('[data-testid="count"]').text()).toBe('2');
- expect(wrapper.findAll(RelatedIssuableItem)).toHaveLength(2);
+ expect(wrapper.findAllComponents(RelatedIssuableItem)).toHaveLength(2);
- const props = wrapper.findAll(RelatedIssuableItem).at(1).props();
+ const props = wrapper.findAllComponents(RelatedIssuableItem).at(1).props();
const data = mockData[1];
expect(props.idKey).toEqual(data.id);
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 12f9707da04..3d027e2084c 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -461,7 +461,7 @@ describe('Issuable output', () => {
describe('when title is not in view', () => {
beforeEach(() => {
wrapper.vm.state.titleText = 'Sticky header title';
- wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
});
it('shows with title', () => {
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index bdb1448148e..9d9abce887b 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -12,6 +12,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -71,7 +72,11 @@ describe('Description component', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
- function createComponent({ props = {}, provide } = {}) {
+ function createComponent({
+ props = {},
+ provide,
+ createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler,
+ } = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
@@ -85,7 +90,7 @@ describe('Description component', () => {
apolloProvider: createMockApollo([
[workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
- [createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler],
+ [createWorkItemFromTaskMutation, createWorkItemFromTaskHandler],
]),
mocks: {
$toast,
@@ -317,7 +322,28 @@ describe('Description component', () => {
expect(findModal().exists()).toBe(false);
});
+ it('shows toast after delete success', async () => {
+ const newDesc = 'description';
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
+
+ expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
+ expect($toast.show).toHaveBeenCalledWith('Task deleted');
+ });
+ });
+
+ describe('creating work item from checklist item', () => {
it('emits `updateDescription` after creating new work item', async () => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ },
+ provide: {
+ glFeatures: {
+ workItemsCreateFromMarkdown: true,
+ },
+ },
+ });
+
const newDescription = `<p>New description</p>`;
await findConvertToTaskButton().trigger('click');
@@ -327,12 +353,28 @@ describe('Description component', () => {
expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
});
- it('shows toast after delete success', async () => {
- const newDesc = 'description';
- findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
+ it('shows flash message when creating task fails', async () => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ },
+ provide: {
+ glFeatures: {
+ workItemsCreateFromMarkdown: true,
+ },
+ },
+ createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}),
+ });
- expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
- expect($toast.show).toHaveBeenCalledWith('Task deleted');
+ await findConvertToTaskButton().trigger('click');
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Something went wrong when creating task. Please try again.',
+ }),
+ );
});
});
diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js
index d58bf1be812..11c43ea4388 100644
--- a/spec/frontend/issues/show/components/edit_actions_spec.js
+++ b/spec/frontend/issues/show/components/edit_actions_spec.js
@@ -2,16 +2,9 @@ import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import IssuableEditActions from '~/issues/show/components/edit_actions.vue';
-import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import eventHub from '~/issues/show/event_hub';
-import {
- getIssueStateQueryResponse,
- updateIssueStateQueryResponse,
-} from '../mock_data/apollo_mock';
describe('Edit Actions component', () => {
let wrapper;
@@ -31,8 +24,6 @@ describe('Edit Actions component', () => {
},
};
- const modalId = 'delete-issuable-modal-1';
-
const createComponent = ({ props, data } = {}) => {
fakeApollo = createMockApollo([], mockResolvers);
@@ -50,16 +41,13 @@ describe('Edit Actions component', () => {
data() {
return {
issueState: {},
- modalId,
...data,
};
},
});
};
- const findModal = () => wrapper.findComponent(DeleteIssueModal);
const findEditButtons = () => wrapper.findAllComponents(GlButton);
- const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button');
const findSaveButton = () => wrapper.findByTestId('issuable-save-button');
const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button');
@@ -79,23 +67,12 @@ describe('Edit Actions component', () => {
});
});
- it('does not render the delete button if canDestroy is false', () => {
- createComponent({ props: { canDestroy: false } });
- expect(findDeleteButton().exists()).toBe(false);
- });
-
it('disables save button when title is blank', () => {
createComponent({ props: { formState: { title: '', issue_type: '' } } });
expect(findSaveButton().attributes('disabled')).toBe('true');
});
- it('does not render the delete button if showDeleteButton is false', () => {
- createComponent({ props: { showDeleteButton: false } });
-
- expect(findDeleteButton().exists()).toBe(false);
- });
-
describe('updateIssuable', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -119,63 +96,4 @@ describe('Edit Actions component', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
});
});
-
- describe('delete issue button', () => {
- let trackingSpy;
-
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('tracks clicking on button', () => {
- findDeleteButton().vm.$emit('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'delete_issue',
- });
- });
- });
-
- describe('delete issue modal', () => {
- it('renders', () => {
- expect(findModal().props()).toEqual({
- issuePath: 'gitlab-org/gitlab-test/-/issues/1',
- issueType: 'Issue',
- modalId,
- title: 'Delete issue',
- });
- });
- });
-
- describe('deleteIssuable', () => {
- beforeEach(() => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- });
-
- it('does not send the `delete.issuable` event when clicking delete button', () => {
- findDeleteButton().vm.$emit('click');
- expect(eventHub.$emit).not.toHaveBeenCalled();
- });
-
- it('sends the `delete.issuable` event when clicking the delete confirm button', async () => {
- expect(eventHub.$emit).toHaveBeenCalledTimes(0);
- findModal().vm.$emit('delete');
- expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable');
- expect(eventHub.$emit).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('with Apollo cache mock', () => {
- it('renders the right delete button text per apollo cache type', async () => {
- mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
- await waitForPromises();
- expect(findDeleteButton().text()).toBe('Delete issue');
- });
-
- it('should not change the delete button text per apollo cache mutation', async () => {
- mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse);
- await waitForPromises();
- expect(findDeleteButton().text()).toBe('Delete issue');
- });
- });
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index d0e33f0b980..61433607a2b 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('Description field component', () => {
let wrapper;
- const findTextarea = () => wrapper.find({ ref: 'textarea' });
+ const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
const mountComponent = (description = 'test') =>
shallowMount(DescriptionField, {
diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js
index de04405d89b..a5fa96d8d64 100644
--- a/spec/frontend/issues/show/components/fields/title_spec.js
+++ b/spec/frontend/issues/show/components/fields/title_spec.js
@@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub';
describe('Title field component', () => {
let wrapper;
- const findInput = () => wrapper.find({ ref: 'input' });
+ const findInput = () => wrapper.findComponent({ ref: 'input' });
beforeEach(() => {
jest.spyOn(eventHub, '$emit');
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 329c4234f30..dc2b3c6fc48 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -65,17 +65,17 @@ describe('HeaderActions component', () => {
},
};
- const findToggleIssueStateButton = () => wrapper.find(GlButton);
+ const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
- const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem);
- const findDesktopDropdownItems = () => findDesktopDropdown().findAll(GlDropdownItem);
+ const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
+ const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
- const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index);
+ const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
const mountComponent = ({
props = {},
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
index 3ab2bb3460b..1286617d64a 100644
--- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -1,13 +1,13 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { GlDatepicker } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql';
import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
+import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
@@ -35,24 +35,21 @@ describe('Create Timeline events', () => {
let responseSpy;
let apolloProvider;
- const findSubmitButton = () => wrapper.findByText(__('Save'));
- const findSubmitAndAddButton = () =>
- wrapper.findByText(s__('Incident|Save and add another event'));
- const findCancelButton = () => wrapper.findByText(__('Cancel'));
+ const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
+ const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
+ const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findNoteInput = () => wrapper.findByTestId('input-note');
const setNoteInput = () => {
- const textarea = findNoteInput().element;
- textarea.value = mockInputData.note;
- textarea.dispatchEvent(new Event('input'));
+ findNoteInput().setValue(mockInputData.note);
};
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
const setDatetime = () => {
const inputDate = new Date(mockInputData.occurredAt);
findDatePicker().vm.$emit('input', inputDate);
- findHourInput().vm.$emit('input', inputDate.getHours());
- findMinuteInput().vm.$emit('input', inputDate.getMinutes());
+ findHourInput().setValue(inputDate.getHours());
+ findMinuteInput().setValue(inputDate.getMinutes());
};
const fillForm = () => {
setDatetime();
diff --git a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
new file mode 100644
index 00000000000..4c1638a9147
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
@@ -0,0 +1,44 @@
+import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
+
+import { mockEvents, fakeEventData, mockInputData } from './mock_data';
+
+describe('Edit Timeline events', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = mountExtended(EditTimelineEvent, {
+ propsData: {
+ event: mockEvents[0],
+ editTimelineEventActive: false,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const findTimelineEventsForm = () => wrapper.findComponent(TimelineEventsForm);
+
+ const mockSaveData = { ...fakeEventData, ...mockInputData };
+
+ describe('editTimelineEvent', () => {
+ const saveEventEvent = { 'handle-save-edit': [[mockSaveData, false]] };
+
+ it('should call the mutation with the right variables', async () => {
+ await findTimelineEventsForm().vm.$emit('save-event', mockSaveData, false);
+
+ expect(wrapper.emitted()).toEqual(saveEventEvent);
+ });
+
+ it('should close the form on cancel', async () => {
+ const cancelEvent = { 'hide-edit': [[]] };
+
+ await findTimelineEventsForm().vm.$emit('cancel');
+
+ expect(wrapper.emitted()).toEqual(cancelEvent);
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
index 155ae703e48..1cfb7d12a91 100644
--- a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
+++ b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
@@ -41,7 +41,7 @@ describe('Highlight Bar', () => {
}
});
- const findLink = () => wrapper.find(GlLink);
+ const findLink = () => wrapper.findComponent(GlLink);
describe('empty state', () => {
beforeEach(() => {
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 8e090645be2..d92aeabba0f 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -61,12 +61,12 @@ describe('Incident Tabs component', () => {
);
};
- const findTabs = () => wrapper.findAll(GlTab);
+ const findTabs = () => wrapper.findAllComponents(GlTab);
const findSummaryTab = () => findTabs().at(0);
const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
- const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
- const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
- const findHighlightBarComponent = () => wrapper.find(HighlightBar);
+ const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
+ const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
+ const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
const findTimelineTab = () => wrapper.findComponent(TimelineTab);
describe('empty state', () => {
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index 75c0a7350ae..adea2b6df59 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -49,6 +49,15 @@ export const mockEvents = [
},
];
+const mockUpdatedEvent = {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/8',
+ note: 'another one23',
+ noteHtml: '<p>another one23</p>',
+ action: 'comment',
+ occurredAt: '2022-07-01T12:47:00Z',
+ createdAt: '2022-07-20T12:47:40Z',
+};
+
export const timelineEventsQueryListResponse = {
data: {
project: {
@@ -93,6 +102,29 @@ export const timelineEventsCreateEventError = {
},
};
+export const timelineEventsEditEventResponse = {
+ data: {
+ timelineEventUpdate: {
+ timelineEvent: {
+ ...mockUpdatedEvent,
+ },
+ errors: [],
+ __typename: 'TimelineEventUpdatePayload',
+ },
+ },
+};
+
+export const timelineEventsEditEventError = {
+ data: {
+ timelineEventUpdate: {
+ timelineEvent: {
+ ...mockUpdatedEvent,
+ },
+ errors: ['Create error'],
+ },
+ },
+};
+
const timelineEventDeleteData = (errors = []) => {
return {
data: {
@@ -125,3 +157,13 @@ export const mockGetTimelineData = {
},
},
};
+
+export const fakeDate = '2020-07-08T00:00:00.000Z';
+
+export const mockInputData = {
+ note: 'test',
+ occurredAt: '2020-08-10T02:30:00.000Z',
+};
+
+const { id, note, occurredAt } = mockEvents[0];
+export const fakeEventData = { id, note, occurredAt };
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index cd2cbb63246..7f086a276f7 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -4,6 +4,8 @@ import { GlDatepicker } from '@gitlab/ui';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
@@ -13,6 +15,8 @@ jest.mock('~/flash');
const fakeDate = '2020-07-08T00:00:00.000Z';
+const mockInputDate = new Date('2021-08-12');
+
describe('Timeline events form', () => {
// July 8 2020
useFakeDate(fakeDate);
@@ -21,7 +25,7 @@ describe('Timeline events form', () => {
const mountComponent = ({ mountMethod = shallowMountExtended }) => {
wrapper = mountMethod(TimelineEventsForm, {
propsData: {
- hasTimelineEvents: true,
+ showSaveAndAdd: true,
isEventProcessed: false,
},
});
@@ -32,17 +36,17 @@ describe('Timeline events form', () => {
wrapper.destroy();
});
- const findSubmitButton = () => wrapper.findByText('Save');
- const findSubmitAndAddButton = () => wrapper.findByText('Save and add another event');
- const findCancelButton = () => wrapper.findByText('Cancel');
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
+ const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
+ const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
- const findDatePickerInput = () => wrapper.findByTestId('input-datepicker');
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
const setDatetime = () => {
- findDatePicker().vm.$emit('input', new Date('2021-08-12'));
- findHourInput().vm.$emit('input', 5);
- findMinuteInput().vm.$emit('input', 45);
+ findDatePicker().vm.$emit('input', mockInputDate);
+ findHourInput().setValue(5);
+ findMinuteInput().setValue(45);
};
const submitForm = async () => {
@@ -58,6 +62,22 @@ describe('Timeline events form', () => {
await waitForPromises();
};
+ it('renders markdown-field component with correct list of toolbar items', () => {
+ mountComponent({ mountMethod: mountExtended });
+
+ expect(findMarkdownField().props('restrictedToolBarItems')).toEqual([
+ 'quote',
+ 'strikethrough',
+ 'bullet-list',
+ 'numbered-list',
+ 'task-list',
+ 'collapsible-section',
+ 'table',
+ 'attach-file',
+ 'full-screen',
+ ]);
+ });
+
describe('form button behaviour', () => {
beforeEach(() => {
mountComponent({ mountMethod: mountExtended });
@@ -87,14 +107,14 @@ describe('Timeline events form', () => {
setDatetime();
await nextTick();
- expect(findDatePickerInput().element.value).toBe('2021-08-12');
+ expect(findDatePicker().props('value')).toBe(mockInputDate);
expect(findHourInput().element.value).toBe('5');
expect(findMinuteInput().element.value).toBe('45');
wrapper.vm.clear();
await nextTick();
- expect(findDatePickerInput().element.value).toBe('2020-07-08');
+ expect(findDatePicker().props('value')).toStrictEqual(new Date(fakeDate));
expect(findHourInput().element.value).toBe('0');
expect(findMinuteInput().element.value).toBe('0');
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
index 90e55003ab3..1bf8d68efd4 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
@@ -1,6 +1,7 @@
import timezoneMock from 'timezone-mock';
import { GlIcon, GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { timelineItemI18n } from '~/issues/show/components/incidents/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue';
import { mockEvents } from './mock_data';
@@ -15,21 +16,19 @@ describe('IncidentTimelineEventList', () => {
action,
noteHtml,
occurredAt,
- isLastItem: false,
...propsData,
},
provide: {
- canUpdate: false,
+ canUpdateTimelineEvent: false,
...provide,
},
});
};
const findCommentIcon = () => wrapper.findComponent(GlIcon);
- const findTextContainer = () => wrapper.findByTestId('event-text-container');
const findEventTime = () => wrapper.findByTestId('event-time');
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDeleteButton = () => wrapper.findByText('Delete');
+ const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
describe('template', () => {
it('shows comment icon', () => {
@@ -50,20 +49,6 @@ describe('IncidentTimelineEventList', () => {
expect(findEventTime().text()).toBe('15:59 UTC');
});
- describe('last item in list', () => {
- it('shows a bottom border when not the last item', () => {
- mountComponent();
-
- expect(findTextContainer().classes()).toContain('gl-border-1');
- });
-
- it('does not show a bottom border when the last item', () => {
- mountComponent({ propsData: { isLastItem: true } });
-
- expect(wrapper.classes()).not.toContain('gl-border-1');
- });
- });
-
describe.each`
timezone
${'Europe/London'}
@@ -96,20 +81,20 @@ describe('IncidentTimelineEventList', () => {
});
it('shows dropdown and delete item when user has update permission', () => {
- mountComponent({ provide: { canUpdate: true } });
+ mountComponent({ provide: { canUpdateTimelineEvent: true } });
expect(findDropdown().exists()).toBe(true);
expect(findDeleteButton().exists()).toBe(true);
});
it('triggers a delete when the delete button is clicked', async () => {
- mountComponent({ provide: { canUpdate: true } });
+ mountComponent({ provide: { canUpdateTimelineEvent: true } });
findDeleteButton().trigger('click');
await nextTick();
- expect(wrapper.emitted().delete).toBeTruthy();
+ expect(wrapper.emitted().delete).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
index 4d2d53c990e..dff1c429d07 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -3,16 +3,24 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
-import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_item.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue';
+import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql';
+import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { useFakeDate } from 'helpers/fake_date';
import { createAlert } from '~/flash';
import {
mockEvents,
timelineEventsDeleteEventResponse,
timelineEventsDeleteEventError,
+ timelineEventsEditEventResponse,
+ timelineEventsEditEventError,
+ fakeDate,
+ fakeEventData,
+ mockInputData,
} from './mock_data';
Vue.use(VueApollo);
@@ -20,83 +28,73 @@ Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-const deleteEventResponse = jest.fn();
-
-function createMockApolloProvider() {
- deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventResponse);
- const requestHandlers = [[deleteTimelineEventMutation, deleteEventResponse]];
- return createMockApollo(requestHandlers);
-}
-
const mockConfirmAction = ({ confirmed }) => {
confirmAction.mockResolvedValueOnce(confirmed);
};
describe('IncidentTimelineEventList', () => {
+ useFakeDate(fakeDate);
let wrapper;
+ const deleteResponseSpy = jest.fn().mockResolvedValue(timelineEventsDeleteEventResponse);
+ const editResponseSpy = jest.fn().mockResolvedValue(timelineEventsEditEventResponse);
- const mountComponent = (mockApollo) => {
- const apollo = mockApollo ? { apolloProvider: mockApollo } : {};
+ const requestHandlers = [
+ [deleteTimelineEventMutation, deleteResponseSpy],
+ [editTimelineEventMutation, editResponseSpy],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMountExtended(IncidentTimelineEventList, {
+ const mountComponent = () => {
+ wrapper = mountExtended(IncidentTimelineEventList, {
+ propsData: {
+ timelineEvents: mockEvents,
+ },
provide: {
fullPath: 'group/project',
issuableId: '1',
+ canUpdateTimelineEvent: true,
},
- propsData: {
- timelineEvents: mockEvents,
- },
- ...apollo,
+ apolloProvider,
});
};
const findTimelineEventGroups = () => wrapper.findAllByTestId('timeline-group');
- const findItems = (base = wrapper) => base.findAll(IncidentTimelineEventListItem);
+ const findItems = (base = wrapper) => base.findAllComponents(IncidentTimelineEventItem);
const findFirstTimelineEventGroup = () => findTimelineEventGroups().at(0);
const findSecondTimelineEventGroup = () => findTimelineEventGroups().at(1);
const findDates = () => wrapper.findAllByTestId('event-date');
const clickFirstDeleteButton = async () => {
- findItems()
- .at(0)
- .vm.$emit('delete', { ...mockEvents[0] });
+ findItems().at(0).vm.$emit('delete', { fakeEventData });
await waitForPromises();
};
+ const clickFirstEditButton = async () => {
+ findItems().at(0).vm.$emit('edit');
+ await waitForPromises();
+ };
+ beforeEach(() => {
+ mountComponent();
+ });
+
afterEach(() => {
- confirmAction.mockReset();
- deleteEventResponse.mockReset();
wrapper.destroy();
});
describe('template', () => {
it('groups items correctly', () => {
- mountComponent();
-
expect(findTimelineEventGroups()).toHaveLength(2);
expect(findItems(findFirstTimelineEventGroup())).toHaveLength(1);
expect(findItems(findSecondTimelineEventGroup())).toHaveLength(2);
});
- it('sets the isLastItem prop correctly', () => {
- mountComponent();
-
- expect(findItems().at(0).props('isLastItem')).toBe(false);
- expect(findItems().at(1).props('isLastItem')).toBe(false);
- expect(findItems().at(2).props('isLastItem')).toBe(true);
- });
-
it('sets the event props correctly', () => {
- mountComponent();
-
expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml);
});
it('formats dates correctly', () => {
- mountComponent();
-
expect(findDates().at(0).text()).toBe('2022-03-22');
expect(findDates().at(1).text()).toBe('2022-03-23');
});
@@ -110,8 +108,6 @@ describe('IncidentTimelineEventList', () => {
describe(timezone, () => {
beforeEach(() => {
timezoneMock.register(timezone);
-
- mountComponent();
});
afterEach(() => {
@@ -131,12 +127,9 @@ describe('IncidentTimelineEventList', () => {
it('should delete when button is clicked', async () => {
const expectedVars = { input: { id: mockEvents[0].id } };
-
- mountComponent(createMockApolloProvider());
-
await clickFirstDeleteButton();
- expect(deleteEventResponse).toHaveBeenCalledWith(expectedVars);
+ expect(deleteResponseSpy).toHaveBeenCalledWith(expectedVars);
});
it('should show an error when delete returns an error', async () => {
@@ -144,8 +137,7 @@ describe('IncidentTimelineEventList', () => {
message: 'Error deleting incident timeline event: Item does not exist',
};
- mountComponent(createMockApolloProvider());
- deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventError);
+ deleteResponseSpy.mockResolvedValue(timelineEventsDeleteEventError);
await clickFirstDeleteButton();
@@ -158,8 +150,7 @@ describe('IncidentTimelineEventList', () => {
error: new Error(),
message: 'Something went wrong while deleting the incident timeline event.',
};
- mountComponent(createMockApolloProvider());
- deleteEventResponse.mockRejectedValueOnce();
+ deleteResponseSpy.mockRejectedValueOnce();
await clickFirstDeleteButton();
@@ -167,4 +158,76 @@ describe('IncidentTimelineEventList', () => {
});
});
});
+
+ describe('Edit Functionality', () => {
+ beforeEach(() => {
+ mountComponent();
+ clickFirstEditButton();
+ });
+
+ const findEditEvent = () => wrapper.findComponent(EditTimelineEvent);
+ const mockSaveData = { ...fakeEventData, ...mockInputData };
+
+ describe('editTimelineEvent', () => {
+ it('should call the mutation with the right variables', async () => {
+ await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await waitForPromises();
+
+ expect(editResponseSpy).toHaveBeenCalledWith({
+ input: mockSaveData,
+ });
+ });
+
+ it('should close the form on successful addition', async () => {
+ await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await waitForPromises();
+
+ expect(findEditEvent().exists()).toBe(false);
+ });
+
+ it('should close the form on cancel', async () => {
+ await findEditEvent().vm.$emit('hide-edit');
+ await waitForPromises();
+
+ expect(findEditEvent().exists()).toBe(false);
+ });
+ });
+
+ describe('error handling', () => {
+ it('should show an error when submission returns an error', async () => {
+ const expectedAlertArgs = {
+ message: `Error updating incident timeline event: ${timelineEventsEditEventError.data.timelineEventUpdate.errors[0]}`,
+ };
+ editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
+
+ await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
+ });
+
+ it('should show an error when submission fails', async () => {
+ const expectedAlertArgs = {
+ captureError: true,
+ error: new Error(),
+ message: 'Something went wrong while updating the incident timeline event.',
+ };
+ editResponseSpy.mockRejectedValueOnce();
+
+ await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
+ });
+
+ it('should keep the form open on failed addition', async () => {
+ editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
+
+ await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await waitForPromises();
+
+ expect(findEditEvent().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
index 2cdb971395d..5bac1d6e7ad 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
@@ -36,7 +36,7 @@ describe('TimelineEventsTab', () => {
provide: {
fullPath: 'group/project',
issuableId: '1',
- canUpdate: true,
+ canUpdateTimelineEvent: true,
...provide,
},
apolloProvider: mockApollo,
@@ -136,29 +136,20 @@ describe('TimelineEventsTab', () => {
it('should not show a button when user cannot update', () => {
mountComponent({
mockApollo: createMockApolloProvider(emptyResponse),
- provide: { canUpdate: false },
+ provide: { canUpdateTimelineEvent: false },
});
expect(findAddEventButton().exists()).toBe(false);
});
it('should not show a form by default', () => {
- expect(findCreateTimelineEvent().isVisible()).toBe(false);
+ expect(findCreateTimelineEvent().exists()).toBe(false);
});
it('should show a form when button is clicked', async () => {
await findAddEventButton().trigger('click');
- expect(findCreateTimelineEvent().isVisible()).toBe(true);
- });
-
- it('should clear the form when button is clicked', async () => {
- const mockClear = jest.fn();
- wrapper.vm.$refs.createEventForm.clearForm = mockClear;
-
- await findAddEventButton().trigger('click');
-
- expect(mockClear).toHaveBeenCalled();
+ expect(findCreateTimelineEvent().exists()).toBe(true);
});
it('should hide the form when the hide event is emitted', async () => {
@@ -167,7 +158,7 @@ describe('TimelineEventsTab', () => {
await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form');
- expect(findCreateTimelineEvent().isVisible()).toBe(false);
+ expect(findCreateTimelineEvent().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
index d3a86680f14..f0494591e95 100644
--- a/spec/frontend/issues/show/components/incidents/utils_spec.js
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock';
import {
displayAndLogError,
getEventIcon,
- getUtcShiftedDateNow,
+ getUtcShiftedDate,
} from '~/issues/show/components/incidents/utils';
import { createAlert } from '~/flash';
@@ -34,7 +34,7 @@ describe('incident utils', () => {
});
});
- describe('getUtcShiftedDateNow', () => {
+ describe('getUtcShiftedDate', () => {
beforeEach(() => {
timezoneMock.register('US/Pacific');
});
@@ -46,7 +46,7 @@ describe('incident utils', () => {
it('should shift the date by the timezone offset', () => {
const date = new Date();
- const shiftedDate = getUtcShiftedDateNow();
+ const shiftedDate = getUtcShiftedDate();
expect(shiftedDate > date).toBe(true);
});
diff --git a/spec/frontend/issues/show/components/pinned_links_spec.js b/spec/frontend/issues/show/components/pinned_links_spec.js
index aac720df6e9..208baac7124 100644
--- a/spec/frontend/issues/show/components/pinned_links_spec.js
+++ b/spec/frontend/issues/show/components/pinned_links_spec.js
@@ -9,7 +9,7 @@ const plainStatusUrl = 'https://status.com';
describe('PinnedLinks', () => {
let wrapper;
- const findButtons = () => wrapper.findAll(GlButton);
+ const findButtons = () => wrapper.findAllComponents(GlButton);
const createComponent = (props) => {
wrapper = shallowMount(PinnedLinks, {
diff --git a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
index b38d2b60057..d4202f4a6ab 100644
--- a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
@@ -62,8 +62,8 @@ describe('Sentry Error Stack Trace', () => {
describe('loading', () => {
it('should show spinner while loading', () => {
mountComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
});
});
@@ -74,8 +74,8 @@ describe('Sentry Error Stack Trace', () => {
it('should show stacktrace', () => {
mountComponent({ stubs: {} });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
index b9fed5f34f1..cc8346253ee 100644
--- a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
+++ b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
@@ -217,7 +217,7 @@ describe('NewBranchForm', () => {
});
it('emits `success` event', () => {
- expect(wrapper.emitted('success')).toBeTruthy();
+ expect(wrapper.emitted('success')).toHaveLength(1);
});
it('called `createBranch` mutation correctly', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js
index 57b11bdbc27..cf496d5836a 100644
--- a/spec/frontend/jira_connect/subscriptions/api_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/api_spec.js
@@ -1,7 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
-import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/subscriptions/api';
+import {
+ axiosInstance,
+ addSubscription,
+ removeSubscription,
+ fetchGroups,
+ getCurrentUser,
+ addJiraConnectSubscription,
+ updateInstallation,
+} from '~/jira_connect/subscriptions/api';
import { getJwt } from '~/jira_connect/subscriptions/utils';
-import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
@@ -9,21 +16,26 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({
}));
describe('JiraConnect API', () => {
- let mock;
+ let axiosMock;
+ let originalGon;
let response;
const mockAddPath = 'addPath';
const mockRemovePath = 'removePath';
const mockNamespace = 'namespace';
const mockJwt = 'jwt';
+ const mockAccessToken = 'accessToken';
const mockResponse = { success: true };
beforeEach(() => {
- mock = new MockAdapter(axios);
+ axiosMock = new MockAdapter(axiosInstance);
+ originalGon = window.gon;
+ window.gon = { api_version: 'v4' };
});
afterEach(() => {
- mock.restore();
+ axiosMock.restore();
+ window.gon = originalGon;
response = null;
});
@@ -31,8 +43,8 @@ describe('JiraConnect API', () => {
const makeRequest = () => addSubscription(mockAddPath, mockNamespace);
it('returns success response', async () => {
- jest.spyOn(axios, 'post');
- mock
+ jest.spyOn(axiosInstance, 'post');
+ axiosMock
.onPost(mockAddPath, {
jwt: mockJwt,
namespace_path: mockNamespace,
@@ -42,7 +54,7 @@ describe('JiraConnect API', () => {
response = await makeRequest();
expect(getJwt).toHaveBeenCalled();
- expect(axios.post).toHaveBeenCalledWith(mockAddPath, {
+ expect(axiosInstance.post).toHaveBeenCalledWith(mockAddPath, {
jwt: mockJwt,
namespace_path: mockNamespace,
});
@@ -54,13 +66,13 @@ describe('JiraConnect API', () => {
const makeRequest = () => removeSubscription(mockRemovePath);
it('returns success response', async () => {
- jest.spyOn(axios, 'delete');
- mock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse);
+ jest.spyOn(axiosInstance, 'delete');
+ axiosMock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse);
response = await makeRequest();
expect(getJwt).toHaveBeenCalled();
- expect(axios.delete).toHaveBeenCalledWith(mockRemovePath, {
+ expect(axiosInstance.delete).toHaveBeenCalledWith(mockRemovePath, {
params: {
jwt: mockJwt,
},
@@ -81,8 +93,8 @@ describe('JiraConnect API', () => {
});
it('returns success response', async () => {
- jest.spyOn(axios, 'get');
- mock
+ jest.spyOn(axiosInstance, 'get');
+ axiosMock
.onGet(mockGroupsPath, {
page: mockPage,
per_page: mockPerPage,
@@ -91,7 +103,7 @@ describe('JiraConnect API', () => {
response = await makeRequest();
- expect(axios.get).toHaveBeenCalledWith(mockGroupsPath, {
+ expect(axiosInstance.get).toHaveBeenCalledWith(mockGroupsPath, {
params: {
page: mockPage,
per_page: mockPerPage,
@@ -100,4 +112,82 @@ describe('JiraConnect API', () => {
expect(response.data).toEqual(mockResponse);
});
});
+
+ describe('getCurrentUser', () => {
+ const makeRequest = () => getCurrentUser();
+
+ it('returns success response', async () => {
+ const expectedUrl = '/api/v4/user';
+
+ jest.spyOn(axiosInstance, 'get');
+
+ axiosMock.onGet(expectedUrl).replyOnce(httpStatus.OK, mockResponse);
+
+ response = await makeRequest();
+
+ expect(axiosInstance.get).toHaveBeenCalledWith(expectedUrl, {});
+ expect(response.data).toEqual(mockResponse);
+ });
+ });
+
+ describe('addJiraConnectSubscription', () => {
+ const makeRequest = () =>
+ addJiraConnectSubscription(mockNamespace, { jwt: mockJwt, accessToken: mockAccessToken });
+
+ it('returns success response', async () => {
+ const expectedUrl = '/api/v4/integrations/jira_connect/subscriptions';
+
+ jest.spyOn(axiosInstance, 'post');
+
+ axiosMock.onPost(expectedUrl).replyOnce(httpStatus.OK, mockResponse);
+
+ response = await makeRequest();
+
+ expect(axiosInstance.post).toHaveBeenCalledWith(
+ expectedUrl,
+ {
+ jwt: mockJwt,
+ namespace_path: mockNamespace,
+ },
+ { headers: { Authorization: `Bearer ${mockAccessToken}` } },
+ );
+ expect(response.data).toEqual(mockResponse);
+ });
+ });
+
+ describe('updateInstallation', () => {
+ const expectedUrl = '/-/jira_connect/installations';
+
+ it.each`
+ instanceUrl | expectedInstanceUrl
+ ${'https://gitlab.com'} | ${null}
+ ${'https://gitlab.mycompany.com'} | ${'https://gitlab.mycompany.com'}
+ `(
+ 'when instanceUrl is $instanceUrl, it passes `instance_url` as $expectedInstanceUrl',
+ async ({ instanceUrl, expectedInstanceUrl }) => {
+ const makeRequest = () => updateInstallation(instanceUrl);
+
+ jest.spyOn(axiosInstance, 'put');
+ axiosMock
+ .onPut(expectedUrl, {
+ jwt: mockJwt,
+ installation: {
+ instance_url: expectedInstanceUrl,
+ },
+ })
+ .replyOnce(httpStatus.OK, mockResponse);
+
+ response = await makeRequest();
+
+ expect(getJwt).toHaveBeenCalled();
+ expect(axiosInstance.put).toHaveBeenCalledWith(expectedUrl, {
+ jwt: mockJwt,
+ installation: {
+ instance_url: expectedInstanceUrl,
+ },
+ });
+ expect(response.data).toEqual(mockResponse);
+ },
+ );
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index d871b1e1dcc..f1fc5e4d90b 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -50,7 +50,7 @@ describe('GroupsList', () => {
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findAllItems = () => wrapper.findAll(GroupsListItem);
+ const findAllItems = () => wrapper.findAllComponents(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
const findSecondItem = () => findAllItems().at(1);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 9894141be5a..369ddda8dbe 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -31,8 +31,8 @@ describe('JiraConnectApp', () => {
const findUserLink = () => wrapper.findComponent(UserLink);
const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
- const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
- store = createStore({ subscriptions: [mockSubscription] });
+ const createComponent = ({ provide, mountFn = shallowMountExtended, initialState = {} } = {}) => {
+ store = createStore({ ...initialState, subscriptions: [mockSubscription] });
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mountFn(JiraConnectApp, {
@@ -60,7 +60,7 @@ describe('JiraConnectApp', () => {
});
it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => {
- expect(findSignInPage().exists()).toBe(shouldRenderSignInPage);
+ expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage);
if (shouldRenderSignInPage) {
expect(findSignInPage().props('hasSubscriptions')).toBe(true);
}
@@ -133,7 +133,7 @@ describe('JiraConnectApp', () => {
});
it('renders link when `linkUrl` is set', async () => {
- createComponent({ mountFn: mountExtended });
+ createComponent({ provide: { usersPath: '' }, mountFn: mountExtended });
store.commit(SET_ALERT, {
message: __('test message %{linkStart}test link%{linkEnd}'),
@@ -211,21 +211,22 @@ describe('JiraConnectApp', () => {
describe('when `jiraConnectOauth` feature flag is enabled', () => {
const mockSubscriptionsPath = '/mockSubscriptionsPath';
- beforeEach(() => {
+ beforeEach(async () => {
jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } });
+ jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true);
createComponent({
+ initialState: {
+ currentUser: { name: 'root' },
+ },
provide: {
glFeatures: { jiraConnectOauth: true },
subscriptionsPath: mockSubscriptionsPath,
},
});
- });
- describe('when component mounts', () => {
- it('dispatches `fetchSubscriptions` action', async () => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath);
- });
+ findSignInPage().vm.$emit('sign-in-oauth');
+ await nextTick();
});
describe('when oauth button emits `sign-in-oauth` event', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index ed0abaaf576..01317eb5dba 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -1,39 +1,41 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import {
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
OAUTH_WINDOW_OPTIONS,
} from '~/jira_connect/subscriptions/constants';
-import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
-import httpStatus from '~/lib/utils/http_status';
import AccessorUtilities from '~/lib/utils/accessor';
-import { getCurrentUser } from '~/rest_api';
+import {
+ getCurrentUser,
+ fetchOAuthApplicationId,
+ fetchOAuthToken,
+} from '~/jira_connect/subscriptions/api';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ACCESS_TOKEN } from '~/jira_connect/subscriptions/store/mutation_types';
jest.mock('~/lib/utils/accessor');
jest.mock('~/jira_connect/subscriptions/utils');
jest.mock('~/jira_connect/subscriptions/api');
-jest.mock('~/rest_api');
jest.mock('~/jira_connect/subscriptions/pkce', () => ({
createCodeVerifier: jest.fn().mockReturnValue('mock-verifier'),
createCodeChallenge: jest.fn().mockResolvedValue('mock-challenge'),
}));
-const mockOauthMetadata = {
- oauth_authorize_url: 'https://gitlab.com/mockOauth',
- oauth_token_url: 'https://gitlab.com/mockOauthToken',
- state: 'good-state',
-};
-
describe('SignInOauthButton', () => {
let wrapper;
- let mockAxios;
let store;
+ const mockOauthMetadata = {
+ oauth_authorize_url: 'https://gitlab.com/mockOauth',
+ oauth_token_path: 'https://gitlab.com/mockOauthToken',
+ oauth_token_payload: {
+ client_id: '543678901',
+ },
+ state: 'good-state',
+ };
const createComponent = ({ slots, props } = {}) => {
store = createStore();
@@ -50,13 +52,8 @@ describe('SignInOauthButton', () => {
});
};
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
- });
-
afterEach(() => {
wrapper.destroy();
- mockAxios.restore();
});
const findButton = () => wrapper.findComponent(GlButton);
@@ -69,6 +66,46 @@ describe('SignInOauthButton', () => {
expect(findButton().props('category')).toBe('primary');
});
+ describe('when `gitlabBasePath` is passed', () => {
+ const mockBasePath = 'https://gitlab.mycompany.com';
+
+ it('uses custom text for button', () => {
+ createComponent({
+ props: {
+ gitlabBasePath: mockBasePath,
+ },
+ });
+
+ expect(findButton().text()).toBe(`Sign in to ${mockBasePath}`);
+ });
+
+ describe('on click', () => {
+ const mockClientId = '798412381';
+
+ beforeEach(async () => {
+ fetchOAuthApplicationId.mockReturnValue({ data: { application_id: mockClientId } });
+ jest.spyOn(window, 'open').mockReturnValue();
+ createComponent({
+ props: {
+ gitlabBasePath: mockBasePath,
+ },
+ });
+
+ findButton().vm.$emit('click');
+
+ await nextTick();
+ });
+
+ it('calls `window.open` with correct arguments', () => {
+ expect(window.open).toHaveBeenCalledWith(
+ `${mockBasePath}/mockOauth?code_challenge=mock-challenge&code_challenge_method=S256&client_id=${mockClientId}`,
+ I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
+ OAUTH_WINDOW_OPTIONS,
+ );
+ });
+ });
+ });
+
it.each`
scenario | cryptoAvailable
${'when crypto API is available'} | ${true}
@@ -96,7 +133,7 @@ describe('SignInOauthButton', () => {
it('calls `window.open` with correct arguments', () => {
expect(window.open).toHaveBeenCalledWith(
- `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256`,
+ `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256&client_id=${mockOauthMetadata.oauth_token_payload.client_id}`,
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
OAUTH_WINDOW_OPTIONS,
);
@@ -151,11 +188,7 @@ describe('SignInOauthButton', () => {
describe('when API requests succeed', () => {
beforeEach(async () => {
- jest.spyOn(axios, 'post');
- jest.spyOn(axios, 'get');
- mockAxios
- .onPost(mockOauthMetadata.oauth_token_url)
- .replyOnce(httpStatus.OK, { access_token: mockAccessToken });
+ fetchOAuthToken.mockResolvedValue({ data: { access_token: mockAccessToken } });
getCurrentUser.mockResolvedValue({ data: mockUser });
window.dispatchEvent(new MessageEvent('message', mockEvent));
@@ -164,9 +197,10 @@ describe('SignInOauthButton', () => {
});
it('executes POST request to Oauth token endpoint', () => {
- expect(axios.post).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_url, {
+ expect(fetchOAuthToken).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_path, {
code: '1234',
code_verifier: 'mock-verifier',
+ client_id: mockOauthMetadata.oauth_token_payload.client_id,
});
});
@@ -185,10 +219,7 @@ describe('SignInOauthButton', () => {
describe('when API requests fail', () => {
beforeEach(async () => {
- jest.spyOn(axios, 'post');
- mockAxios
- .onPost(mockOauthMetadata.oauth_token_url)
- .replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ fetchOAuthToken.mockRejectedValue();
window.dispatchEvent(new MessageEvent('message', mockEvent));
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
index 1649920b48b..b9a8451f3b3 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
@@ -101,7 +101,7 @@ describe('SignInGitlabCom', () => {
const button = findSignInOauthButton();
button.vm.$emit('error');
- expect(wrapper.emitted('error')).toBeTruthy();
+ expect(wrapper.emitted('error')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
index f4be8bf121d..10696d25f17 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -5,9 +5,22 @@ import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in
import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
+import { updateInstallation } from '~/jira_connect/subscriptions/api';
+import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
+
+jest.mock('~/jira_connect/subscriptions/api', () => {
+ return {
+ updateInstallation: jest.fn(),
+ setApiBaseURL: jest.fn(),
+ };
+});
+jest.mock('~/jira_connect/subscriptions/utils');
+
describe('SignInGitlabMultiversion', () => {
let wrapper;
+ const mockBasePath = 'gitlab.mycompany.com';
+
const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
const findSubtitle = () => wrapper.findByTestId('subtitle');
@@ -29,30 +42,32 @@ describe('SignInGitlabMultiversion', () => {
});
describe('when form emits "submit" event', () => {
- it('hides the version select form and shows the sign in button', async () => {
+ it('updates the backend, then saves the baseUrl and reloads', async () => {
+ updateInstallation.mockResolvedValue({});
+
createComponent();
- findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com');
+ findVersionSelectForm().vm.$emit('submit', mockBasePath);
await nextTick();
- expect(findVersionSelectForm().exists()).toBe(false);
- expect(findSignInOauthButton().exists()).toBe(true);
+ expect(updateInstallation).toHaveBeenCalled();
+ expect(persistBaseUrl).toHaveBeenCalledWith(mockBasePath);
+ expect(reloadPage).toHaveBeenCalled();
});
});
});
});
describe('when version is selected', () => {
- beforeEach(async () => {
+ beforeEach(() => {
+ retrieveBaseUrl.mockReturnValue(mockBasePath);
createComponent();
-
- findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com');
- await nextTick();
});
describe('sign in button', () => {
it('renders sign in button', () => {
expect(findSignInOauthButton().exists()).toBe(true);
+ expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
});
describe('when button emits `sign-in` event', () => {
@@ -71,7 +86,7 @@ describe('SignInGitlabMultiversion', () => {
const button = findSignInOauthButton();
button.vm.$emit('error');
- expect(wrapper.emitted('error')).toBeTruthy();
+ expect(wrapper.emitted('error')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
index 53b5d8e70af..5e3c30269b5 100644
--- a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
@@ -8,8 +8,6 @@ import {
} from '~/jira_connect/subscriptions/store/actions';
import state from '~/jira_connect/subscriptions/store/state';
import * as api from '~/jira_connect/subscriptions/api';
-import * as userApi from '~/api/user_api';
-import * as integrationsApi from '~/api/integrations_api';
import {
I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE,
I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
@@ -79,7 +77,7 @@ describe('JiraConnect actions', () => {
describe('when API request succeeds', () => {
it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => {
const mockUser = { name: 'root' };
- jest.spyOn(userApi, 'getCurrentUser').mockResolvedValue({ data: mockUser });
+ jest.spyOn(api, 'getCurrentUser').mockResolvedValue({ data: mockUser });
await testAction(
loadCurrentUser,
@@ -89,7 +87,7 @@ describe('JiraConnect actions', () => {
[],
);
- expect(userApi.getCurrentUser).toHaveBeenCalledWith({
+ expect(api.getCurrentUser).toHaveBeenCalledWith({
headers: { Authorization: `Bearer ${mockAccessToken}` },
});
});
@@ -97,7 +95,7 @@ describe('JiraConnect actions', () => {
describe('when API request fails', () => {
it('commits the SET_CURRENT_USER_ERROR mutation', async () => {
- jest.spyOn(userApi, 'getCurrentUser').mockRejectedValue();
+ jest.spyOn(api, 'getCurrentUser').mockRejectedValue();
await testAction(
loadCurrentUser,
@@ -120,9 +118,7 @@ describe('JiraConnect actions', () => {
describe('when API request succeeds', () => {
it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => {
- jest
- .spyOn(integrationsApi, 'addJiraConnectSubscription')
- .mockResolvedValue({ success: true });
+ jest.spyOn(api, 'addJiraConnectSubscription').mockResolvedValue({ success: true });
await testAction(
addSubscription,
@@ -144,7 +140,7 @@ describe('JiraConnect actions', () => {
[{ type: 'fetchSubscriptions', payload: mockSubscriptionsPath }],
);
- expect(integrationsApi.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, {
+ expect(api.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, {
accessToken: null,
jwt: '1234',
});
@@ -153,7 +149,7 @@ describe('JiraConnect actions', () => {
describe('when API request fails', () => {
it('commits the SET_CURRENT_USER_ERROR mutation', async () => {
- jest.spyOn(integrationsApi, 'addJiraConnectSubscription').mockRejectedValue();
+ jest.spyOn(api, 'addJiraConnectSubscription').mockRejectedValue();
await testAction(
addSubscription,
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index cd8024d4962..022a0f81aaa 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -21,15 +21,15 @@ describe('JiraImportApp', () => {
const setupIllustration = 'setup-illustration.svg';
- const getFormComponent = () => wrapper.find(JiraImportForm);
+ const getFormComponent = () => wrapper.findComponent(JiraImportForm);
- const getProgressComponent = () => wrapper.find(JiraImportProgress);
+ const getProgressComponent = () => wrapper.findComponent(JiraImportProgress);
- const getSetupComponent = () => wrapper.find(JiraImportSetup);
+ const getSetupComponent = () => wrapper.findComponent(JiraImportSetup);
- const getAlert = () => wrapper.find(GlAlert);
+ const getAlert = () => wrapper.findComponent(GlAlert);
- const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const mountComponent = ({
isJiraConfigured = true,
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 41d3cd46d01..d43a9f8a145 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -164,8 +164,9 @@ describe('JiraImportForm', () => {
it('shows a heading for the user mapping section', () => {
expect(
- getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }),
- ).toBeTruthy();
+ getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' })
+ .innerText,
+ ).toBe('Jira-GitLab user mapping template');
});
it('shows information to the user', () => {
@@ -182,15 +183,15 @@ describe('JiraImportForm', () => {
});
it('has a "Jira display name" column', () => {
- expect(getHeader('Jira display name')).toBeTruthy();
+ expect(getHeader('Jira display name').innerText).toBe('Jira display name');
});
it('has an "arrow" column', () => {
- expect(getHeader('Arrow')).toBeTruthy();
+ expect(getHeader('Arrow').getAttribute('aria-label')).toBe('Arrow');
});
it('has a "GitLab username" column', () => {
- expect(getHeader('GitLab username')).toBeTruthy();
+ expect(getHeader('GitLab username').innerText).toBe('GitLab username');
});
});
@@ -288,8 +289,8 @@ describe('JiraImportForm', () => {
});
it('updates the user list', () => {
- expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1);
- expect(getUserDropdown().find(GlDropdownItem).text()).toContain(
+ expect(getUserDropdown().findAllComponents(GlDropdownItem)).toHaveLength(1);
+ expect(getUserDropdown().findComponent(GlDropdownItem).text()).toContain(
'fchopin (Frederic Chopin)',
);
});
diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js
index 04b2a2da622..42356763492 100644
--- a/spec/frontend/jira_import/components/jira_import_progress_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js
@@ -8,7 +8,7 @@ describe('JiraImportProgress', () => {
const importProject = 'JIRAPROJECT';
- const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
+ const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
const getParagraphText = () => wrapper.find('p').text();
diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js
index 320e270b493..0085a2b5572 100644
--- a/spec/frontend/jira_import/components/jira_import_setup_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js
@@ -6,7 +6,7 @@ import { illustration, jiraIntegrationPath } from '../mock_data';
describe('JiraImportSetup', () => {
let wrapper;
- const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
+ const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
beforeEach(() => {
wrapper = shallowMount(JiraImportSetup, {
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
index 322cfa3ba1f..98bdfc3fcbc 100644
--- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -15,23 +15,27 @@ describe('Jobs filtered search', () => {
const findStatusToken = () => getSearchToken('status');
- const createComponent = () => {
- wrapper = shallowMount(JobsFilteredSearch);
+ const createComponent = (props) => {
+ wrapper = shallowMount(JobsFilteredSearch, {
+ propsData: {
+ ...props,
+ },
+ });
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
it('displays filtered search', () => {
+ createComponent();
+
expect(findFilteredSearch().exists()).toBe(true);
});
it('displays status token', () => {
+ createComponent();
+
expect(findStatusToken()).toMatchObject({
type: 'status',
icon: 'status',
@@ -42,8 +46,26 @@ describe('Jobs filtered search', () => {
});
it('emits filter token to parent component', () => {
+ createComponent();
+
findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
});
+
+ it('filtered search value is empty array when no query string is passed', () => {
+ createComponent();
+
+ expect(findFilteredSearch().props('value')).toEqual([]);
+ });
+
+ it('filtered search returns correct data shape when passed query string', () => {
+ const value = 'SUCCESS';
+
+ createComponent({ queryString: { statuses: value } });
+
+ expect(findFilteredSearch().props('value')).toEqual([
+ { type: 'status', value: { data: value, operator: '=' } },
+ ]);
+ });
});
diff --git a/spec/frontend/jobs/components/filtered_search/utils_spec.js b/spec/frontend/jobs/components/filtered_search/utils_spec.js
new file mode 100644
index 00000000000..8440ab42b86
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/utils_spec.js
@@ -0,0 +1,19 @@
+import { validateQueryString } from '~/jobs/components/filtered_search/utils';
+
+describe('Filtered search utils', () => {
+ describe('validateQueryString', () => {
+ it.each`
+ queryStringObject | expected
+ ${{ statuses: 'SUCCESS' }} | ${{ statuses: 'SUCCESS' }}
+ ${{ statuses: 'failed' }} | ${{ statuses: 'FAILED' }}
+ ${{ wrong: 'SUCCESS' }} | ${null}
+ ${{ statuses: 'wrong' }} | ${null}
+ ${{ wrong: 'wrong' }} | ${null}
+ `(
+ 'when provided $queryStringObject, the expected result is $expected',
+ ({ queryStringObject, expected }) => {
+ expect(validateQueryString(queryStringObject)).toEqual(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/jobs/components/artifacts_block_spec.js b/spec/frontend/jobs/components/job/artifacts_block_spec.js
index 0c7c0a6c311..c75deb64d84 100644
--- a/spec/frontend/jobs/components/artifacts_block_spec.js
+++ b/spec/frontend/jobs/components/job/artifacts_block_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import ArtifactsBlock from '~/jobs/components/artifacts_block.vue';
+import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Artifacts block', () => {
diff --git a/spec/frontend/jobs/components/commit_block_spec.js b/spec/frontend/jobs/components/job/commit_block_spec.js
index 8a6d48cecb8..4fcc754c82c 100644
--- a/spec/frontend/jobs/components/commit_block_spec.js
+++ b/spec/frontend/jobs/components/job/commit_block_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CommitBlock from '~/jobs/components/commit_block.vue';
+import CommitBlock from '~/jobs/components/job/sidebar/commit_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('Commit block', () => {
diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js
index 9738fd14275..299b607ad78 100644
--- a/spec/frontend/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/job/empty_state_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import EmptyState from '~/jobs/components/empty_state.vue';
+import EmptyState from '~/jobs/components/job/empty_state.vue';
describe('Empty State', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/environments_block_spec.js b/spec/frontend/jobs/components/job/environments_block_spec.js
index d90c9137a8f..134533e2af8 100644
--- a/spec/frontend/jobs/components/environments_block_spec.js
+++ b/spec/frontend/jobs/components/job/environments_block_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
+import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
const TEST_CLUSTER_NAME = 'test_cluster';
const TEST_CLUSTER_PATH = 'path/to/test_cluster';
@@ -46,7 +46,7 @@ describe('Environments block', () => {
});
};
- const findText = () => wrapper.find(EnvironmentsBlock).text();
+ const findText = () => wrapper.findComponent(EnvironmentsBlock).text();
const findJobDeploymentLink = () => wrapper.find('[data-testid="job-deployment-link"]');
const findEnvironmentLink = () => wrapper.find('[data-testid="job-environment-link"]');
const findClusterLink = () => wrapper.find('[data-testid="job-cluster-link"]');
diff --git a/spec/frontend/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/job/erased_block_spec.js
index 057df20ccc2..c6aba01fa53 100644
--- a/spec/frontend/jobs/components/erased_block_spec.js
+++ b/spec/frontend/jobs/components/job/erased_block_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import ErasedBlock from '~/jobs/components/erased_block.vue';
+import ErasedBlock from '~/jobs/components/job/erased_block.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Erased block', () => {
@@ -10,7 +10,7 @@ describe('Erased block', () => {
const timeago = getTimeago();
const formattedDate = timeago.format(erasedAt);
- const findLink = () => wrapper.find(GlLink);
+ const findLink = () => wrapper.findComponent(GlLink);
const createComponent = (props) => {
wrapper = mount(ErasedBlock, {
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index b4b5bc4669d..822528403cf 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -5,16 +5,16 @@ import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import { TEST_HOST } from 'helpers/test_constants';
-import EmptyState from '~/jobs/components/empty_state.vue';
-import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
-import ErasedBlock from '~/jobs/components/erased_block.vue';
-import JobApp from '~/jobs/components/job_app.vue';
-import Sidebar from '~/jobs/components/sidebar.vue';
-import StuckBlock from '~/jobs/components/stuck_block.vue';
-import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
+import EmptyState from '~/jobs/components/job/empty_state.vue';
+import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
+import ErasedBlock from '~/jobs/components/job/erased_block.vue';
+import JobApp from '~/jobs/components/job/job_app.vue';
+import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
+import StuckBlock from '~/jobs/components/job/stuck_block.vue';
+import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
import createStore from '~/jobs/store';
import axios from '~/lib/utils/axios_utils';
-import job from '../mock_data';
+import job from '../../mock_data';
describe('Job App', () => {
Vue.use(Vuex);
@@ -57,18 +57,18 @@ describe('Job App', () => {
await nextTick();
};
- const findLoadingComponent = () => wrapper.find(GlLoadingIcon);
- const findSidebar = () => wrapper.find(Sidebar);
+ const findLoadingComponent = () => wrapper.findComponent(GlLoadingIcon);
+ const findSidebar = () => wrapper.findComponent(Sidebar);
const findJobContent = () => wrapper.find('[data-testid="job-content"');
- const findStuckBlockComponent = () => wrapper.find(StuckBlock);
+ const findStuckBlockComponent = () => wrapper.findComponent(StuckBlock);
const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"');
const findStuckBlockNoActiveRunners = () =>
wrapper.find('[data-testid="job-stuck-no-active-runners"');
- const findFailedJobComponent = () => wrapper.find(UnmetPrerequisitesBlock);
- const findEnvironmentsBlockComponent = () => wrapper.find(EnvironmentsBlock);
- const findErasedBlock = () => wrapper.find(ErasedBlock);
+ const findFailedJobComponent = () => wrapper.findComponent(UnmetPrerequisitesBlock);
+ const findEnvironmentsBlockComponent = () => wrapper.findComponent(EnvironmentsBlock);
+ const findErasedBlock = () => wrapper.findComponent(ErasedBlock);
const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]');
- const findEmptyState = () => wrapper.find(EmptyState);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]');
const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
const findJobLogScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job/job_container_item_spec.js
index eb2b0184e5f..05c38dd74b7 100644
--- a/spec/frontend/jobs/components/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job/job_container_item_spec.js
@@ -2,9 +2,9 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
-import JobContainerItem from '~/jobs/components/job_container_item.vue';
+import JobContainerItem from '~/jobs/components/job/sidebar/job_container_item.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import job from '../mock_data';
+import job from '../../mock_data';
describe('JobContainerItem', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
index aa85253a177..5e9a73b4387 100644
--- a/spec/frontend/jobs/components/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
@@ -1,10 +1,10 @@
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import JobLogControllers from '~/jobs/components/job_log_controllers.vue';
+import JobLogControllers from '~/jobs/components/job/job_log_controllers.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import * as commonUtils from '~/lib/utils/common_utils';
-import { mockJobLog } from '../mock_data';
+import { mockJobLog } from '../../mock_data';
const mockToastShow = jest.fn();
diff --git a/spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
index 08973223c08..d60043f33f7 100644
--- a/spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js
+++ b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
@@ -1,17 +1,17 @@
import { GlLink, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import JobRetryForwardDeploymentModal from '~/jobs/components/job_retry_forward_deployment_modal.vue';
+import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants';
import createStore from '~/jobs/store';
-import job from '../mock_data';
+import job from '../../mock_data';
describe('Job Retry Forward Deployment Modal', () => {
let store;
let wrapper;
const retryOutdatedJobDocsUrl = 'url-to-docs';
- const findLink = () => wrapper.find(GlLink);
- const findModal = () => wrapper.find(GlModal);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findModal = () => wrapper.findComponent(GlModal);
const createWrapper = ({ props = {}, provide = {}, stubs = {} } = {}) => {
store = createStore();
diff --git a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js
index 4046f0269dd..4da17ed8366 100644
--- a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DetailRow from '~/jobs/components/sidebar_detail_row.vue';
-import SidebarJobDetailsContainer from '~/jobs/components/sidebar_job_details_container.vue';
+import DetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue';
+import SidebarJobDetailsContainer from '~/jobs/components/job/sidebar/sidebar_job_details_container.vue';
import createStore from '~/jobs/store';
-import job from '../mock_data';
+import job from '../../mock_data';
describe('Job Sidebar Details Container', () => {
let store;
@@ -11,7 +11,7 @@ describe('Job Sidebar Details Container', () => {
const findJobTimeout = () => wrapper.findByTestId('job-timeout');
const findJobTags = () => wrapper.findByTestId('job-tags');
- const findAllDetailsRow = () => wrapper.findAll(DetailRow);
+ const findAllDetailsRow = () => wrapper.findAllComponents(DetailRow);
const createWrapper = ({ props = {} } = {}) => {
store = createStore();
diff --git a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
index ad72b9be261..18d5f35bde4 100644
--- a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue';
+import JobsSidebarRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
import createStore from '~/jobs/store';
-import job from '../mock_data';
+import job from '../../mock_data';
describe('Job Sidebar Retry Button', () => {
let store;
diff --git a/spec/frontend/jobs/components/jobs_container_spec.js b/spec/frontend/jobs/components/job/jobs_container_spec.js
index 127570b8184..2fde4d3020b 100644
--- a/spec/frontend/jobs/components/jobs_container_spec.js
+++ b/spec/frontend/jobs/components/job/jobs_container_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import JobsContainer from '~/jobs/components/jobs_container.vue';
+import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
describe('Jobs List block', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js
new file mode 100644
index 00000000000..184562b2968
--- /dev/null
+++ b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js
@@ -0,0 +1,156 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue';
+
+Vue.use(Vuex);
+
+describe('Manual Variables Form', () => {
+ let wrapper;
+ let store;
+
+ const requiredProps = {
+ action: {
+ path: '/play',
+ method: 'post',
+ button_title: 'Trigger this manual action',
+ },
+ };
+
+ const createComponent = (props = {}) => {
+ store = new Vuex.Store({
+ actions: {
+ triggerManualJob: jest.fn(),
+ },
+ });
+
+ wrapper = extendedWrapper(
+ mount(LegacyManualVariablesForm, {
+ propsData: { ...requiredProps, ...props },
+ store,
+ stubs: {
+ GlSprintf,
+ },
+ }),
+ );
+ };
+
+ const findHelpText = () => wrapper.findComponent(GlSprintf);
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+
+ const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
+ const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
+ const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
+ const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
+ const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
+ const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
+ const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
+ const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
+
+ const setCiVariableKey = () => {
+ findCiVariableKey().setValue('new key');
+ findCiVariableKey().vm.$emit('change');
+ nextTick();
+ };
+
+ const setCiVariableKeyByPosition = (position, value) => {
+ findAllCiVariableKeys().at(position).setValue(value);
+ findAllCiVariableKeys().at(position).vm.$emit('change');
+ nextTick();
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('creates a new variable when user enters a new key value', async () => {
+ expect(findAllVariables()).toHaveLength(1);
+
+ await setCiVariableKey();
+
+ expect(findAllVariables()).toHaveLength(2);
+ });
+
+ it('does not create extra empty variables', async () => {
+ expect(findAllVariables()).toHaveLength(1);
+
+ await setCiVariableKey();
+
+ expect(findAllVariables()).toHaveLength(2);
+
+ await setCiVariableKey();
+
+ expect(findAllVariables()).toHaveLength(2);
+ });
+
+ it('removes the correct variable row', async () => {
+ const variableKeyNameOne = 'key-one';
+ const variableKeyNameThree = 'key-three';
+
+ await setCiVariableKeyByPosition(0, variableKeyNameOne);
+
+ await setCiVariableKeyByPosition(1, 'key-two');
+
+ await setCiVariableKeyByPosition(2, variableKeyNameThree);
+
+ expect(findAllVariables()).toHaveLength(4);
+
+ await findAllDeleteVarBtns().at(1).trigger('click');
+
+ expect(findAllVariables()).toHaveLength(3);
+
+ expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
+ expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
+ expect(findAllCiVariableKeys().at(2).element.value).toBe('');
+ });
+
+ it('trigger button is disabled after trigger action', async () => {
+ expect(findTriggerBtn().props('disabled')).toBe(false);
+
+ await findTriggerBtn().trigger('click');
+
+ expect(findTriggerBtn().props('disabled')).toBe(true);
+ });
+
+ it('delete variable button should only show when there is more than one variable', async () => {
+ expect(findDeleteVarBtn().exists()).toBe(false);
+
+ await setCiVariableKey();
+
+ expect(findDeleteVarBtn().exists()).toBe(true);
+ });
+
+ it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+ });
+
+ it('renders help text with provided link', () => {
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
+ );
+ });
+
+ it('passes variables in correct format', async () => {
+ jest.spyOn(store, 'dispatch');
+
+ await setCiVariableKey();
+
+ await findCiVariableValue().setValue('new value');
+
+ await findTriggerBtn().trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
+ {
+ key: 'new key',
+ secret_value: 'new value',
+ },
+ ]);
+ });
+});
diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
new file mode 100644
index 00000000000..cb32ca9d3dc
--- /dev/null
+++ b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
@@ -0,0 +1,91 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
+import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
+import createStore from '~/jobs/store';
+import job from '../../mock_data';
+
+describe('Legacy Sidebar Header', () => {
+ let store;
+ let wrapper;
+
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findRetryButton = () => wrapper.findComponent(JobRetryButton);
+ const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
+
+ const createWrapper = (props) => {
+ store = createStore();
+
+ wrapper = extendedWrapper(
+ shallowMount(LegacySidebarHeader, {
+ propsData: {
+ job,
+ ...props,
+ },
+ store,
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when job log is erasable', () => {
+ const path = '/root/ci-project/-/jobs/1447/erase';
+
+ beforeEach(() => {
+ createWrapper({
+ erasePath: path,
+ });
+ });
+
+ it('renders erase job link', () => {
+ expect(findEraseLink().exists()).toBe(true);
+ });
+
+ it('erase job link has correct path', () => {
+ expect(findEraseLink().attributes('href')).toBe(path);
+ });
+ });
+
+ describe('when job log is not erasable', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('does not render erase button', () => {
+ expect(findEraseLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when the job is retryable', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('should render the retry button', () => {
+ expect(findRetryButton().props('href')).toBe(job.retry_path);
+ });
+ });
+
+ describe('when there is no retry path', () => {
+ it('should not render a retry button', async () => {
+ const copy = { ...job, retry_path: null };
+ createWrapper({ job: copy });
+
+ expect(findRetryButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when the job is cancelable', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('should render link to cancel job', () => {
+ expect(findCancelButton().props('icon')).toBe('cancel');
+ expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index 6faab3ddf31..5806f9f75f9 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import ManualVariablesForm from '~/jobs/components/manual_variables_form.vue';
+import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
Vue.use(Vuex);
diff --git a/spec/frontend/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
index 8d2680608ab..5c9c011b4ab 100644
--- a/spec/frontend/jobs/components/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import SidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue';
+import SidebarDetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue';
describe('Sidebar detail row', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js
new file mode 100644
index 00000000000..cb32ca9d3dc
--- /dev/null
+++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js
@@ -0,0 +1,91 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
+import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
+import createStore from '~/jobs/store';
+import job from '../../mock_data';
+
+describe('Legacy Sidebar Header', () => {
+ let store;
+ let wrapper;
+
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findRetryButton = () => wrapper.findComponent(JobRetryButton);
+ const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
+
+ const createWrapper = (props) => {
+ store = createStore();
+
+ wrapper = extendedWrapper(
+ shallowMount(LegacySidebarHeader, {
+ propsData: {
+ job,
+ ...props,
+ },
+ store,
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when job log is erasable', () => {
+ const path = '/root/ci-project/-/jobs/1447/erase';
+
+ beforeEach(() => {
+ createWrapper({
+ erasePath: path,
+ });
+ });
+
+ it('renders erase job link', () => {
+ expect(findEraseLink().exists()).toBe(true);
+ });
+
+ it('erase job link has correct path', () => {
+ expect(findEraseLink().attributes('href')).toBe(path);
+ });
+ });
+
+ describe('when job log is not erasable', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('does not render erase button', () => {
+ expect(findEraseLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when the job is retryable', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('should render the retry button', () => {
+ expect(findRetryButton().props('href')).toBe(job.retry_path);
+ });
+ });
+
+ describe('when there is no retry path', () => {
+ it('should not render a retry button', async () => {
+ const copy = { ...job, retry_path: null };
+ createWrapper({ job: copy });
+
+ expect(findRetryButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when the job is cancelable', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('should render link to cancel job', () => {
+ expect(findCancelButton().props('icon')).toBe('cancel');
+ expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index 39c71986ce4..dc1aa67489d 100644
--- a/spec/frontend/jobs/components/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -1,27 +1,23 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import ArtifactsBlock from '~/jobs/components/artifacts_block.vue';
-import JobRetryForwardDeploymentModal from '~/jobs/components/job_retry_forward_deployment_modal.vue';
-import JobRetryButton from '~/jobs/components/job_sidebar_retry_button.vue';
-import JobsContainer from '~/jobs/components/jobs_container.vue';
-import Sidebar, { forwardDeploymentFailureModalId } from '~/jobs/components/sidebar.vue';
-import StagesDropdown from '~/jobs/components/stages_dropdown.vue';
+import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
+import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
+import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
+import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
+import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue';
import createStore from '~/jobs/store';
-import job, { jobsInStage } from '../mock_data';
+import job, { jobsInStage } from '../../mock_data';
describe('Sidebar details block', () => {
let store;
let wrapper;
const forwardDeploymentFailure = 'forward_deployment_failure';
- const findModal = () => wrapper.find(JobRetryForwardDeploymentModal);
+ const findModal = () => wrapper.findComponent(JobRetryForwardDeploymentModal);
const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock);
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
- const findRetryButton = () => wrapper.find(JobRetryButton);
const findTerminalLink = () => wrapper.findByTestId('terminal-link');
- const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
const createWrapper = (props) => {
store = createStore();
@@ -43,44 +39,6 @@ describe('Sidebar details block', () => {
wrapper.destroy();
});
- describe('when job log is erasable', () => {
- const path = '/root/ci-project/-/jobs/1447/erase';
-
- beforeEach(() => {
- createWrapper({
- erasePath: path,
- });
- });
-
- it('renders erase job link', () => {
- expect(findEraseLink().exists()).toBe(true);
- });
-
- it('erase job link has correct path', () => {
- expect(findEraseLink().attributes('href')).toBe(path);
- });
- });
-
- describe('when job log is not erasable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('does not render erase button', () => {
- expect(findEraseLink().exists()).toBe(false);
- });
- });
-
- describe('when there is no retry path retry', () => {
- it('should not render a retry button', async () => {
- createWrapper();
- const copy = { ...job, retry_path: null };
- await store.dispatch('receiveJobSuccess', copy);
-
- expect(findRetryButton().exists()).toBe(false);
- });
- });
-
describe('without terminal path', () => {
it('does not render terminal link', async () => {
createWrapper();
@@ -109,15 +67,6 @@ describe('Sidebar details block', () => {
expect(findNewIssueButton().attributes('href')).toBe(job.new_issue_path);
expect(findNewIssueButton().text()).toBe('New issue');
});
-
- it('should render the retry button', () => {
- expect(findRetryButton().props('href')).toBe(job.retry_path);
- });
-
- it('should render link to cancel job', () => {
- expect(findCancelButton().props('icon')).toBe('cancel');
- expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
- });
});
describe('forward deployment failure', () => {
@@ -155,16 +104,6 @@ describe('Sidebar details block', () => {
it('should render the modal', () => {
expect(findModal().exists()).toBe(true);
});
-
- it('should provide the modal id to the button and modal', () => {
- expect(findRetryButton().props('modalId')).toBe(forwardDeploymentFailureModalId);
- expect(findModal().props('modalId')).toBe(forwardDeploymentFailureModalId);
- });
-
- it('should provide the retry path to the button and modal', () => {
- expect(findRetryButton().props('href')).toBe(job.retry_path);
- expect(findModal().props('href')).toBe(job.retry_path);
- });
});
});
@@ -176,7 +115,7 @@ describe('Sidebar details block', () => {
describe('with stages', () => {
it('renders value provided as selectedStage as selected', () => {
- expect(wrapper.find(StagesDropdown).props('selectedStage')).toBe('aStage');
+ expect(wrapper.findComponent(StagesDropdown).props('selectedStage')).toBe('aStage');
});
});
@@ -184,7 +123,7 @@ describe('Sidebar details block', () => {
beforeEach(() => store.dispatch('receiveJobSuccess', job));
it('does not render jobs container', () => {
- expect(wrapper.find(JobsContainer).exists()).toBe(false);
+ expect(wrapper.findComponent(JobsContainer).exists()).toBe(false);
});
});
@@ -195,7 +134,7 @@ describe('Sidebar details block', () => {
});
it('renders list of jobs', () => {
- expect(wrapper.find(JobsContainer).exists()).toBe(true);
+ expect(wrapper.findComponent(JobsContainer).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/stages_dropdown_spec.js b/spec/frontend/jobs/components/job/stages_dropdown_spec.js
index f638213ef0c..61dec585e82 100644
--- a/spec/frontend/jobs/components/stages_dropdown_spec.js
+++ b/spec/frontend/jobs/components/job/stages_dropdown_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Mousetrap from 'mousetrap';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import StagesDropdown from '~/jobs/components/stages_dropdown.vue';
+import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import * as copyToClipboard from '~/behaviors/copy_to_clipboard';
import {
@@ -10,7 +10,7 @@ import {
mockPipelineWithoutMR,
mockPipelineWithAttachedMR,
mockPipelineDetached,
-} from '../mock_data';
+} from '../../mock_data';
describe('Stages Dropdown', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/job/stuck_block_spec.js
index 1580ed45e46..8dc570cce27 100644
--- a/spec/frontend/jobs/components/stuck_block_spec.js
+++ b/spec/frontend/jobs/components/job/stuck_block_spec.js
@@ -1,6 +1,6 @@
import { GlBadge, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import StuckBlock from '~/jobs/components/stuck_block.vue';
+import StuckBlock from '~/jobs/components/job/stuck_block.vue';
describe('Stuck Block Job component', () => {
let wrapper;
@@ -26,8 +26,8 @@ describe('Stuck Block Job component', () => {
wrapper.find('[data-testid="job-stuck-no-active-runners"]');
const findStuckNoRunners = () => wrapper.find('[data-testid="job-stuck-no-runners"]');
const findStuckWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"]');
- const findRunnerPathLink = () => wrapper.find(GlLink);
- const findAllBadges = () => wrapper.findAll(GlBadge);
+ const findRunnerPathLink = () => wrapper.findComponent(GlLink);
+ const findAllBadges = () => wrapper.findAllComponents(GlBadge);
describe('with no runners for project', () => {
beforeEach(() => {
diff --git a/spec/frontend/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/job/trigger_block_spec.js
index 78596612d23..a1de8fd143f 100644
--- a/spec/frontend/jobs/components/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/job/trigger_block_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import TriggerBlock from '~/jobs/components/trigger_block.vue';
+import TriggerBlock from '~/jobs/components/job/sidebar/trigger_block.vue';
describe('Trigger block', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
index aeb85694e60..fb7d389c4d6 100644
--- a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
@@ -1,6 +1,6 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
+import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
describe('Unmet Prerequisites Block Job component', () => {
let wrapper;
@@ -23,7 +23,7 @@ describe('Unmet Prerequisites Block Job component', () => {
});
it('renders an alert with the correct message', () => {
- const container = wrapper.find(GlAlert);
+ const container = wrapper.findComponent(GlAlert);
const alertMessage =
'This job failed because the necessary resources were not successfully created.';
@@ -32,7 +32,7 @@ describe('Unmet Prerequisites Block Job component', () => {
});
it('renders link to help page', () => {
- const helpLink = wrapper.find(GlLink);
+ const helpLink = wrapper.findComponent(GlLink);
expect(helpLink).not.toBeNull();
expect(helpLink.text()).toContain('More information');
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index bdc8ae0eef0..ec8e79bba13 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -39,7 +39,7 @@ describe('Job Log Header Line', () => {
});
it('renders the line number component', () => {
- expect(wrapper.find(LineNumber).exists()).toBe(true);
+ expect(wrapper.findComponent(LineNumber).exists()).toBe(true);
});
it('renders a span the provided text', () => {
@@ -90,7 +90,7 @@ describe('Job Log Header Line', () => {
});
it('renders the duration badge', () => {
- expect(wrapper.find(DurationBadge).exists()).toBe(true);
+ expect(wrapper.findComponent(DurationBadge).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js
index bf80d90e299..50ebd1610d2 100644
--- a/spec/frontend/jobs/components/log/line_spec.js
+++ b/spec/frontend/jobs/components/log/line_spec.js
@@ -42,7 +42,7 @@ describe('Job Log Line', () => {
});
it('renders the line number component', () => {
- expect(wrapper.find(LineNumber).exists()).toBe(true);
+ expect(wrapper.findComponent(LineNumber).exists()).toBe(true);
});
it('renders a span the provided text', () => {
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 374768c3ee4..8c724a8030b 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -11,12 +11,14 @@ import VueApollo from 'vue-apollo';
import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'spec/test_constants';
import createFlash from '~/flash';
import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import {
mockJobsResponsePaginated,
mockJobsResponseEmpty,
@@ -230,5 +232,17 @@ describe('Job table app', () => {
expect(createFlash).toHaveBeenCalledWith(expectedWarning);
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
});
+
+ it('updates URL query string when filtering jobs by status', async () => {
+ createComponent();
+
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
index b9f97a3c3ae..0d11c4d56bf 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -111,7 +111,7 @@ describe('Job State actions', () => {
});
describe('success', () => {
- it('dispatches requestJob and receiveJobSuccess ', () => {
+ it('dispatches requestJob and receiveJobSuccess', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
return testAction(
@@ -137,7 +137,7 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
- it('dispatches requestJob and receiveJobError ', () => {
+ it('dispatches requestJob and receiveJobError', () => {
return testAction(
fetchJob,
null,
@@ -291,7 +291,7 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
});
- it('dispatches requestJobLog and receiveJobLogError ', () => {
+ it('dispatches requestJobLog and receiveJobLogError', () => {
return testAction(
fetchJobLog,
null,
@@ -355,7 +355,7 @@ describe('Job State actions', () => {
window.clearTimeout = origTimeout;
});
- it('should commit STOP_POLLING_JOB_LOG mutation ', async () => {
+ it('should commit STOP_POLLING_JOB_LOG mutation', async () => {
const jobLogTimeout = 7;
await testAction(
@@ -370,7 +370,7 @@ describe('Job State actions', () => {
});
describe('receiveJobLogSuccess', () => {
- it('should commit RECEIVE_JOB_LOG_SUCCESS mutation ', () => {
+ it('should commit RECEIVE_JOB_LOG_SUCCESS mutation', () => {
return testAction(
receiveJobLogSuccess,
'hello world',
@@ -388,7 +388,7 @@ describe('Job State actions', () => {
});
describe('toggleCollapsibleLine', () => {
- it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', () => {
+ it('should commit TOGGLE_COLLAPSIBLE_LINE mutation', () => {
return testAction(
toggleCollapsibleLine,
{ isClosed: true },
@@ -400,7 +400,7 @@ describe('Job State actions', () => {
});
describe('requestJobsForStage', () => {
- it('should commit REQUEST_JOBS_FOR_STAGE mutation ', () => {
+ it('should commit REQUEST_JOBS_FOR_STAGE mutation', () => {
return testAction(
requestJobsForStage,
{ name: 'deploy' },
@@ -423,7 +423,7 @@ describe('Job State actions', () => {
});
describe('success', () => {
- it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', () => {
+ it('dispatches requestJobsForStage and receiveJobsForStageSuccess', () => {
mock
.onGet(`${TEST_HOST}/jobs.json`)
.replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
@@ -473,7 +473,7 @@ describe('Job State actions', () => {
});
describe('receiveJobsForStageSuccess', () => {
- it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', () => {
+ it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation', () => {
return testAction(
receiveJobsForStageSuccess,
[{ id: 121212, name: 'karma' }],
@@ -485,7 +485,7 @@ describe('Job State actions', () => {
});
describe('receiveJobsForStageError', () => {
- it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', () => {
+ it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation', () => {
return testAction(
receiveJobsForStageError,
null,
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index ea1ec383d6e..89cda3b0544 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -83,7 +83,7 @@ describe('Jobs Store Mutations', () => {
describe('with new job log', () => {
describe('log.lines', () => {
describe('when append is true', () => {
- it('sets the parsed log ', () => {
+ it('sets the parsed log', () => {
mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: true,
size: 511846,
@@ -107,7 +107,7 @@ describe('Jobs Store Mutations', () => {
});
describe('when it is defined', () => {
- it('sets the parsed log ', () => {
+ it('sets the parsed log', () => {
mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: false,
size: 511846,
diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js
index 6204138f885..24a803d3f16 100644
--- a/spec/frontend/labels/components/delete_label_modal_spec.js
+++ b/spec/frontend/labels/components/delete_label_modal_spec.js
@@ -34,7 +34,7 @@ describe('~/labels/components/delete_label_modal', () => {
wrapper.destroy();
});
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
describe('template', () => {
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index 29b927ef628..5523cc0606e 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -203,7 +203,7 @@ describe('~/lib/dompurify', () => {
expect(el.getAttribute('rel')).toBe('noreferrer noopener');
});
- it('does not update `rel` values when target is not `_blank` ', () => {
+ it('does not update `rel` values when target is not `_blank`', () => {
const html = `<a href="https://example.com" target="_self" rel="help">internal</a>`;
const el = getSanitizedNode(html);
diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js
index f53f809b799..7c383ae68a4 100644
--- a/spec/frontend/lib/gfm/index_spec.js
+++ b/spec/frontend/lib/gfm/index_spec.js
@@ -24,12 +24,6 @@ describe('gfm', () => {
};
describe('render', () => {
- it('processes Commonmark and provides an ast to the renderer function', async () => {
- const result = await markdownToAST('This is text');
-
- expect(result.type).toBe('root');
- });
-
it('transforms raw HTML into individual nodes in the AST', async () => {
const result = await markdownToAST('<strong>This is bold text</strong>');
@@ -46,216 +40,270 @@ describe('gfm', () => {
);
});
- it('returns the result of executing the renderer function', async () => {
- const rendered = { value: 'rendered tree' };
+ describe('with custom renderer', () => {
+ it('processes Commonmark and provides an ast to the renderer function', async () => {
+ const result = await markdownToAST('This is text');
- const result = await render({
- markdown: '<strong>This is bold text</strong>',
- renderer: () => {
- return rendered;
- },
+ expect(result.type).toBe('root');
});
- expect(result).toEqual(rendered);
+ it('returns the result of executing the renderer function', async () => {
+ const rendered = { value: 'rendered tree' };
+
+ const result = await render({
+ markdown: '<strong>This is bold text</strong>',
+ renderer: () => {
+ return rendered;
+ },
+ });
+
+ expect(result).toEqual(rendered);
+ });
});
- describe('when skipping the rendering of footnote reference and definition nodes', () => {
- it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
- const result = await markdownToAST(
- `footnote reference [^footnote]
+ describe('footnote references and footnote definitions', () => {
+ describe('when skipping the rendering of footnote reference and definition nodes', () => {
+ it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
+ const result = await markdownToAST(
+ `footnote reference [^footnote]
[^footnote]: Footnote definition`,
- ['footnoteReference', 'footnoteDefinition'],
- );
+ ['footnoteReference', 'footnoteDefinition'],
+ );
- expectInRoot(
- result,
- expect.objectContaining({
- children: expect.arrayContaining([
- expect.objectContaining({
- type: 'element',
- tagName: 'footnotereference',
- properties: {
- identifier: 'footnote',
- label: 'footnote',
- },
- }),
- ]),
- }),
- );
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'footnotereference',
+ properties: {
+ identifier: 'footnote',
+ label: 'footnote',
+ },
+ }),
+ ]),
+ }),
+ );
- expectInRoot(
- result,
- expect.objectContaining({
- tagName: 'footnotedefinition',
- properties: {
- identifier: 'footnote',
- label: 'footnote',
- },
- }),
- );
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'footnotedefinition',
+ properties: {
+ identifier: 'footnote',
+ label: 'footnote',
+ },
+ }),
+ );
+ });
});
});
- describe('when skipping the rendering of code blocks', () => {
- it('transforms code nodes into codeblock html tags', async () => {
- const result = await markdownToAST(
- `
+ describe('code blocks', () => {
+ describe('when skipping the rendering of code blocks', () => {
+ it('transforms code nodes into codeblock html tags', async () => {
+ const result = await markdownToAST(
+ `
\`\`\`javascript
console.log('Hola');
\`\`\`\
`,
- ['code'],
- );
+ ['code'],
+ );
- expectInRoot(
- result,
- expect.objectContaining({
- tagName: 'codeblock',
- properties: {
- language: 'javascript',
- },
- }),
- );
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'codeblock',
+ properties: {
+ language: 'javascript',
+ },
+ }),
+ );
+ });
});
});
- describe('when skipping the rendering of reference definitions', () => {
- it('transforms code nodes into codeblock html tags', async () => {
- const result = await markdownToAST(
- `
+ describe('reference definitions', () => {
+ describe('when skipping the rendering of reference definitions', () => {
+ it('transforms code nodes into codeblock html tags', async () => {
+ const result = await markdownToAST(
+ `
[gitlab][gitlab]
[gitlab]: https://gitlab.com "GitLab"
`,
- ['definition'],
- );
+ ['definition'],
+ );
- expectInRoot(
- result,
- expect.objectContaining({
- type: 'element',
- tagName: 'referencedefinition',
- properties: {
- identifier: 'gitlab',
- title: 'GitLab',
- url: 'https://gitlab.com',
- },
- children: [
- {
- type: 'text',
- value: '[gitlab]: https://gitlab.com "GitLab"',
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'referencedefinition',
+ properties: {
+ identifier: 'gitlab',
+ title: 'GitLab',
+ url: 'https://gitlab.com',
},
- ],
- }),
- );
+ children: [
+ {
+ type: 'text',
+ value: '[gitlab]: https://gitlab.com "GitLab"',
+ },
+ ],
+ }),
+ );
+ });
});
});
- describe('when skipping the rendering of link and image references', () => {
- it('transforms linkReference and imageReference nodes into html tags', async () => {
- const result = await markdownToAST(
- `
+ describe('link and image references', () => {
+ describe('when skipping the rendering of link and image references', () => {
+ it('transforms linkReference and imageReference nodes into html tags', async () => {
+ const result = await markdownToAST(
+ `
[gitlab][gitlab] and ![GitLab Logo][gitlab-logo]
[gitlab]: https://gitlab.com "GitLab"
[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"
`,
- ['linkReference', 'imageReference'],
- );
+ ['linkReference', 'imageReference'],
+ );
- expectInRoot(
- result,
- expect.objectContaining({
- tagName: 'p',
- children: expect.arrayContaining([
- expect.objectContaining({
- type: 'element',
- tagName: 'a',
- properties: expect.objectContaining({
- href: 'https://gitlab.com',
- isReference: 'true',
- identifier: 'gitlab',
- title: 'GitLab',
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'p',
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'a',
+ properties: expect.objectContaining({
+ href: 'https://gitlab.com',
+ isReference: 'true',
+ identifier: 'gitlab',
+ title: 'GitLab',
+ }),
}),
- }),
- expect.objectContaining({
- type: 'element',
- tagName: 'img',
- properties: expect.objectContaining({
- src: 'https://gitlab.com/gitlab-logo.png',
- isReference: 'true',
- identifier: 'gitlab-logo',
- title: 'GitLab Logo',
- alt: 'GitLab Logo',
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'img',
+ properties: expect.objectContaining({
+ src: 'https://gitlab.com/gitlab-logo.png',
+ isReference: 'true',
+ identifier: 'gitlab-logo',
+ title: 'GitLab Logo',
+ alt: 'GitLab Logo',
+ }),
}),
- }),
- ]),
- }),
- );
- });
+ ]),
+ }),
+ );
+ });
- it('normalizes the urls extracted from the reference definitions', async () => {
- const result = await markdownToAST(
- `
+ it('normalizes the urls extracted from the reference definitions', async () => {
+ const result = await markdownToAST(
+ `
[gitlab][gitlab] and ![GitLab Logo][gitlab]
[gitlab]: /url\\bar*baz
`,
- ['linkReference', 'imageReference'],
- );
+ ['linkReference', 'imageReference'],
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'p',
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'a',
+ properties: expect.objectContaining({
+ href: '/url%5Cbar*baz',
+ }),
+ }),
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'img',
+ properties: expect.objectContaining({
+ src: '/url%5Cbar*baz',
+ }),
+ }),
+ ]),
+ }),
+ );
+ });
+ });
+ });
+
+ describe('frontmatter', () => {
+ describe('when skipping the rendering of frontmatter types', () => {
+ it.each`
+ type | input
+ ${'yaml'} | ${'---\ntitle: page\n---'}
+ ${'toml'} | ${'+++\ntitle: page\n+++'}
+ ${'json'} | ${';;;\ntitle: page\n;;;'}
+ `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => {
+ const result = await markdownToAST(input, [type]);
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'frontmatter',
+ properties: {
+ language: type,
+ },
+ children: [
+ {
+ type: 'text',
+ value: 'title: page',
+ },
+ ],
+ }),
+ );
+ });
+ });
+ });
+
+ describe('table of contents', () => {
+ it.each`
+ markdown
+ ${'[[_TOC_]]'}
+ ${' [[_TOC_]]'}
+ ${'[[_TOC_]] '}
+ ${'[TOC]'}
+ ${' [TOC]'}
+ ${'[TOC] '}
+ `('parses $markdown and produces a table of contents section', async ({ markdown }) => {
+ const result = await markdownToAST(markdown);
expectInRoot(
result,
expect.objectContaining({
- tagName: 'p',
- children: expect.arrayContaining([
- expect.objectContaining({
- type: 'element',
- tagName: 'a',
- properties: expect.objectContaining({
- href: '/url%5Cbar*baz',
- }),
- }),
- expect.objectContaining({
- type: 'element',
- tagName: 'img',
- properties: expect.objectContaining({
- src: '/url%5Cbar*baz',
- }),
- }),
- ]),
+ type: 'element',
+ tagName: 'nav',
}),
);
});
});
- });
- describe('when skipping the rendering of frontmatter types', () => {
- it.each`
- type | input
- ${'yaml'} | ${'---\ntitle: page\n---'}
- ${'toml'} | ${'+++\ntitle: page\n+++'}
- ${'json'} | ${';;;\ntitle: page\n;;;'}
- `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => {
- const result = await markdownToAST(input, [type]);
+ describe('when skipping the rendering of table of contents', () => {
+ it('transforms table of contents nodes into html tableofcontents tags', async () => {
+ const result = await markdownToAST('[[_TOC_]]', ['tableOfContents']);
- expectInRoot(
- result,
- expect.objectContaining({
- type: 'element',
- tagName: 'frontmatter',
- properties: {
- language: type,
- },
- children: [
- {
- type: 'text',
- value: 'title: page',
- },
- ],
- }),
- );
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'tableofcontents',
+ }),
+ );
+ });
});
});
});
diff --git a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
index 06573f346e0..b972f669ac4 100644
--- a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
+++ b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
@@ -84,7 +84,7 @@ describe('StartupJSLink', () => {
});
});
- describe('variable match errors: ', () => {
+ describe('variable match errors:', () => {
it('forwards requests if the variables are not matching', () => {
window.gl = {
startup_graphql_calls: [
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index a2ace8857ed..a0140d1d8a8 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -476,7 +476,7 @@ describe('common_utils', () => {
});
});
- it('catches the rejected promise from the callback ', () => {
+ it('catches the rejected promise from the callback', () => {
const errorMessage = 'Mistakes were made!';
return commonUtils
.backOff((next, stop) => {
diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
index 47bb512cbb5..59b3b4c02df 100644
--- a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
@@ -1,4 +1,4 @@
-import { newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
+import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
describe('newDateAsLocaleTime', () => {
it.each`
@@ -15,3 +15,19 @@ describe('newDateAsLocaleTime', () => {
expect(newDateAsLocaleTime(string)).toEqual(expected);
});
});
+
+describe('getDateWithUTC', () => {
+ it.each`
+ date | expected
+ ${new Date('2022-03-22T01:23:45.678Z')} | ${new Date('2022-03-22T00:00:00.000Z')}
+ ${new Date('1999-12-31T23:59:59.999Z')} | ${new Date('1999-12-31T00:00:00.000Z')}
+ ${2022} | ${null}
+ ${[]} | ${null}
+ ${{}} | ${null}
+ ${true} | ${null}
+ ${null} | ${null}
+ ${undefined} | ${null}
+ `('returns $expected given $string', ({ date, expected }) => {
+ expect(getDateWithUTC(date)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/finite_state_machine_spec.js b/spec/frontend/lib/utils/finite_state_machine_spec.js
index 441dd24c758..cfde3b8596e 100644
--- a/spec/frontend/lib/utils/finite_state_machine_spec.js
+++ b/spec/frontend/lib/utils/finite_state_machine_spec.js
@@ -50,13 +50,13 @@ describe('Finite State Machine', () => {
});
it('throws an error if the machine definition is invalid', () => {
- expect(() => machine(badDefinition)).toThrowError(
+ expect(() => machine(badDefinition)).toThrow(
'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)',
);
});
it('throws an error if the initial state is invalid', () => {
- expect(() => machine(unstartableDefinition)).toThrowError(
+ expect(() => machine(unstartableDefinition)).toThrow(
`Cannot initialize the state machine to state '${STATE_IMPOSSIBLE}'. Is that one of the machine's defined states?`,
);
});
diff --git a/spec/frontend/lib/utils/is_navigating_away_spec.js b/spec/frontend/lib/utils/is_navigating_away_spec.js
index e1230fe96bf..b8a01a1706c 100644
--- a/spec/frontend/lib/utils/is_navigating_away_spec.js
+++ b/spec/frontend/lib/utils/is_navigating_away_spec.js
@@ -6,7 +6,7 @@ describe('isNavigatingAway', () => {
setNavigatingForTestsOnly(false);
});
- it.each([false, true])('it returns the navigation flag with value %s', (flag) => {
+ it.each([false, true])('returns the navigation flag with value %s', (flag) => {
setNavigatingForTestsOnly(flag);
expect(isNavigatingAway()).toEqual(flag);
});
diff --git a/spec/frontend/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js
index 6d3a871eb33..4dbd50223d5 100644
--- a/spec/frontend/lib/utils/navigation_utility_spec.js
+++ b/spec/frontend/lib/utils/navigation_utility_spec.js
@@ -63,7 +63,7 @@ describe('initPrefetchLinks', () => {
expect(newLink.addEventListener).toHaveBeenCalled();
});
- it('it is not fired when less then 100ms over link', () => {
+ it('is not fired when less then 100ms over link', () => {
const mouseOverEvent = new Event('mouseover');
const mouseOutEvent = new Event('mouseout');
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 1f150599983..94a5f5385b7 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -128,9 +128,11 @@ describe('Poll', () => {
errorCallback: callbacks.error,
});
+ expect(Polling.timeoutID).toBeNull();
+
Polling.makeDelayedRequest(1);
- expect(Polling.timeoutID).toBeTruthy();
+ expect(Polling.timeoutID).not.toBeNull();
return waitForAllCallsToFinish(2, () => {
Polling.stop();
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 733d89fe08c..8d179baa505 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -586,6 +586,33 @@ describe('init markdown', () => {
);
});
+ it('only converts valid URLs', () => {
+ const notValidUrl = 'group::label';
+ const expectedUrlValue = 'url';
+ const expectedText = `other [${notValidUrl}](${expectedUrlValue}) text`;
+ const initialValue = `other ${notValidUrl} text`;
+
+ textArea.value = initialValue;
+ selectedIndex = initialValue.indexOf(notValidUrl);
+ textArea.setSelectionRange(selectedIndex, selectedIndex + notValidUrl.length);
+
+ insertMarkdownText({
+ textArea,
+ text: textArea.value,
+ tag,
+ blockTag: null,
+ selected: notValidUrl,
+ wrap: false,
+ select,
+ });
+
+ expect(textArea.value).toEqual(expectedText);
+ expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedUrlValue, 1));
+ expect(textArea.selectionEnd).toEqual(
+ expectedText.indexOf(expectedUrlValue, 1) + expectedUrlValue.length,
+ );
+ });
+
it('adds block tags on line above and below selection', () => {
selected = 'this text\nis multiple\nlines';
text = `before \n${selected}\nafter `;
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 8e31fc792c5..49a160c9f23 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -45,29 +45,18 @@ describe('text_utility', () => {
});
describe('slugify', () => {
- it('should remove accents and convert to lower case', () => {
- expect(textUtils.slugify('João')).toEqual('jo-o');
- });
- it('should replaces whitespaces with hyphens and convert to lower case', () => {
- expect(textUtils.slugify('My Input String')).toEqual('my-input-string');
- });
- it('should remove trailing whitespace and replace whitespaces within string with a hyphen', () => {
- expect(textUtils.slugify(' a new project ')).toEqual('a-new-project');
- });
- it('should only remove non-allowed special characters', () => {
- expect(textUtils.slugify('test!_pro-ject~')).toEqual('test-_pro-ject');
- });
- it('should squash multiple hypens', () => {
- expect(textUtils.slugify('test!!!!_pro-ject~')).toEqual('test-_pro-ject');
- });
- it('should return empty string if only non-allowed characters', () => {
- expect(textUtils.slugify('здрасти')).toEqual('');
- });
- it('should squash multiple separators', () => {
- expect(textUtils.slugify('Test:-)')).toEqual('test');
- });
- it('should trim any separators from the beginning and end of the slug', () => {
- expect(textUtils.slugify('-Test:-)-')).toEqual('test');
+ it.each`
+ title | input | output
+ ${'should remove accents and convert to lower case'} | ${'João'} | ${'jo-o'}
+ ${'should replaces whitespaces with hyphens and convert to lower case'} | ${'My Input String'} | ${'my-input-string'}
+ ${'should remove trailing whitespace and replace whitespaces within string with a hyphen'} | ${' a new project '} | ${'a-new-project'}
+ ${'should only remove non-allowed special characters'} | ${'test!_pro-ject~'} | ${'test-_pro-ject'}
+ ${'should squash to multiple non-allowed special characters'} | ${'test!!!!_pro-ject~'} | ${'test-_pro-ject'}
+ ${'should return empty string if only non-allowed characters'} | ${'дружба'} | ${''}
+ ${'should squash multiple separators'} | ${'Test:-)'} | ${'test'}
+ ${'should trim any separators from the beginning and end of the slug'} | ${'-Test:-)-'} | ${'test'}
+ `('$title', ({ input, output }) => {
+ expect(textUtils.slugify(input)).toBe(output);
});
});
diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
index 1821a15f677..d25a692dfea 100644
--- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js
+++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
@@ -128,7 +128,7 @@ describe('~/lib/utils/vuex_module_mappers', () => {
describe('with non-string object value', () => {
it('throws helpful error', () => {
- expect(() => mapVuexModuleActions((vm) => vm.bogus, { foo: () => {} })).toThrowError(
+ expect(() => mapVuexModuleActions((vm) => vm.bogus, { foo: () => {} })).toThrow(
REQUIRE_STRING_ERROR_MESSAGE,
);
});
diff --git a/spec/frontend/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js
index 52e903b819f..e0d0e117ea4 100644
--- a/spec/frontend/locale/sprintf_spec.js
+++ b/spec/frontend/locale/sprintf_spec.js
@@ -63,12 +63,26 @@ describe('locale', () => {
it('does not escape parameters for escapeParameters = false', () => {
const input = 'contains %{safeContent}';
const parameters = {
- safeContent: '<strong>bold attempt</strong>',
+ safeContent: '15',
};
const output = sprintf(input, parameters, false);
- expect(output).toBe('contains <strong>bold attempt</strong>');
+ expect(output).toBe('contains 15');
+ });
+
+ describe('replaces duplicated % in input', () => {
+ it('removes duplicated percentage signs', () => {
+ const input = 'contains duplicated %{safeContent}%%';
+
+ const parameters = {
+ safeContent: '15',
+ };
+
+ const output = sprintf(input, parameters, false);
+
+ expect(output).toBe('contains duplicated 15%');
+ });
});
});
});
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 9b908e5b6f0..9172876e76f 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,7 +1,7 @@
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 06ccd107ce3..49c4c46c3ac 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -23,6 +23,7 @@ export const member = {
webUrl: 'https://gitlab.com/root',
avatarUrl: 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
blocked: false,
+ isBot: false,
twoFactorEnabled: false,
oncallSchedules: [{ name: 'schedule 1' }],
escalationPolicies: [{ name: 'policy 1' }],
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index d37e6871387..20dce639177 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -69,7 +69,7 @@ describe('Vuex members actions', () => {
payload: { error },
},
]),
- ).rejects.toThrowError(error);
+ ).rejects.toThrow(error);
});
});
});
@@ -122,7 +122,7 @@ describe('Vuex members actions', () => {
payload: { error },
},
]),
- ).rejects.toThrowError(error);
+ ).rejects.toThrow(error);
});
});
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index b0c9459ff4f..0271483801c 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -1,5 +1,12 @@
import setWindowLocation from 'helpers/set_window_location_helper';
-import { DEFAULT_SORT, MEMBER_TYPES } from '~/members/constants';
+import {
+ DEFAULT_SORT,
+ MEMBER_TYPES,
+ I18N_USER_YOU,
+ I18N_USER_BLOCKED,
+ I18N_USER_BOT,
+ I188N_USER_2FA,
+} from '~/members/constants';
import {
generateBadges,
isGroup,
@@ -52,9 +59,10 @@ describe('Members Utils', () => {
it.each`
member | expected
- ${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }}
- ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }}
- ${member2faEnabled} | ${{ show: true, text: '2FA', variant: 'info' }}
+ ${memberMock} | ${{ show: true, text: I18N_USER_YOU, variant: 'success' }}
+ ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: I18N_USER_BLOCKED, variant: 'danger' }}
+ ${{ ...memberMock, user: { ...memberMock.user, isBot: true } }} | ${{ show: true, text: I18N_USER_BOT, variant: 'muted' }}
+ ${member2faEnabled} | ${{ show: true, text: I188N_USER_2FA, variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(
generateBadges({ member, isCurrentUser: true, canManageMembers: true }),
diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
index 4fdc4024e10..9b5641ef7b3 100644
--- a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
+++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
@@ -49,8 +49,8 @@ describe('Merge Conflict Resolver App', () => {
extendedWrapper(w).findByTestId('interactive-button');
const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button');
const findSideBySideButton = () => wrapper.findByTestId('side-by-side');
- const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines);
- const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines);
+ const findInlineConflictLines = (w = wrapper) => w.findComponent(InlineConflictLines);
+ const findParallelConflictLines = (w = wrapper) => w.findComponent(ParallelConflictLines);
const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message');
it('shows the amount of conflicts', () => {
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 7cee6576b53..e73769cba51 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -48,7 +48,7 @@ describe('merge conflicts actions', () => {
);
});
- it('when data has type equal to error ', () => {
+ it('when data has type equal to error', () => {
mock.onGet(conflictsPath).reply(200, { type: 'error', message: 'error message' });
return testAction(
actions.fetchConflictsData,
@@ -63,7 +63,7 @@ describe('merge conflicts actions', () => {
);
});
- it('when request fails ', () => {
+ it('when request fails', () => {
mock.onGet(conflictsPath).reply(400);
return testAction(
actions.fetchConflictsData,
@@ -80,7 +80,7 @@ describe('merge conflicts actions', () => {
});
describe('setConflictsData', () => {
- it('INTERACTIVE_RESOLVE_MODE updates the correct file ', () => {
+ it('INTERACTIVE_RESOLVE_MODE updates the correct file', () => {
decorateFiles.mockReturnValue([{ bar: 'baz' }]);
return testAction(
actions.setConflictsData,
@@ -239,7 +239,7 @@ describe('merge conflicts actions', () => {
});
describe('setFileResolveMode', () => {
- it('INTERACTIVE_RESOLVE_MODE updates the correct file ', () => {
+ it('INTERACTIVE_RESOLVE_MODE updates the correct file', () => {
return testAction(
actions.setFileResolveMode,
{ file: files[0], mode: INTERACTIVE_RESOLVE_MODE },
@@ -257,7 +257,7 @@ describe('merge conflicts actions', () => {
);
});
- it('EDIT_RESOLVE_MODE updates the correct file ', async () => {
+ it('EDIT_RESOLVE_MODE updates the correct file', async () => {
restoreFileLinesState.mockReturnValue([]);
const file = {
...files[0],
@@ -286,7 +286,7 @@ describe('merge conflicts actions', () => {
});
describe('setPromptConfirmationState', () => {
- it('updates the correct file ', () => {
+ it('updates the correct file', () => {
return testAction(
actions.setPromptConfirmationState,
{ file: files[0], promptDiscardConfirmation: true },
@@ -315,7 +315,7 @@ describe('merge conflicts actions', () => {
],
};
- it('updates the correct file ', async () => {
+ it('updates the correct file', async () => {
const marLikeMockReturn = { foo: 'bar' };
markLine.mockReturnValue(marLikeMockReturn);
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 2001bb5f95e..c6e90a4b20d 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -333,7 +333,7 @@ describe('MergeRequestTabs', () => {
${'show'} | ${false} | ${'shows'}
${'diffs'} | ${true} | ${'hides'}
${'commits'} | ${true} | ${'hides'}
- `('it $hidesText expand button on $tab tab', ({ tab, hides }) => {
+ `('$hidesText expand button on $tab tab', ({ tab, hides }) => {
window.gon = { features: { movedMrSidebar: true } };
const expandButton = document.createElement('div');
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index a8e3d13dca0..ce5b2a1000b 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -96,19 +96,19 @@ describe('Milestone combobox component', () => {
const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findProjectMilestonesSection = () =>
wrapper.find('[data-testid="project-milestones-section"]');
const findProjectMilestonesDropdownItems = () =>
- findProjectMilestonesSection().findAll(GlDropdownItem);
+ findProjectMilestonesSection().findAllComponents(GlDropdownItem);
const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
const findGroupMilestonesDropdownItems = () =>
- findGroupMilestonesSection().findAll(GlDropdownItem);
+ findGroupMilestonesSection().findAllComponents(GlDropdownItem);
const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
//
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 14f04d9b767..263d6225a9f 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -3,7 +3,8 @@
exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="prometheus-graphs"
- data-qa-selector="prometheus_graphs"
+ data-qa-selector="prometheus_graphs_content"
+ data-testid="prometheus-graphs"
environmentstate="available"
metricsdashboardbasepath="/monitoring/monitor-project/-/metrics?environment=1"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
@@ -60,6 +61,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
clearalltext="Clear all"
clearalltextclass="gl-px-5"
data-qa-selector="environments_dropdown"
+ data-testid="environments-dropdown"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
index 91fe36bc6e4..779ded090c2 100644
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
@@ -72,7 +72,7 @@ describe('Stacked column chart component', () => {
]);
});
- it('chart options should configure data zoom and axis label ', () => {
+ it('chart options should configure data zoom and axis label', () => {
const chartOptions = findChart().props('option');
const xAxisType = findChart().props('xAxisType');
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index d797d9e2ad0..339c1710a9e 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -430,7 +430,7 @@ describe('Dashboard Panel', () => {
expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
});
- it('it renders a time series chart with no errors', () => {
+ it('renders a time series chart with no errors', () => {
expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 608404e5c5b..1de6b6e3e98 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -407,7 +407,7 @@ describe('Dashboard', () => {
await nextTick();
});
- it('it does not show loading icons in any group', async () => {
+ it('does not show loading icons in any group', async () => {
setupStoreWithData(store);
await nextTick();
@@ -614,11 +614,11 @@ describe('Dashboard', () => {
const findFirstDraggableRemoveButton = () =>
findDraggablePanels().at(0).find('.js-draggable-remove');
- it('it enables draggables', async () => {
+ it('enables draggables', async () => {
findRearrangeButton().vm.$emit('click');
await nextTick();
- expect(findRearrangeButton().attributes('pressed')).toBeTruthy();
+ expect(findRearrangeButton().attributes('pressed')).toBe('true');
expect(findEnabledDraggables().wrappers).toEqual(findDraggables().wrappers);
});
@@ -656,13 +656,13 @@ describe('Dashboard', () => {
expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1);
});
- it('it disables draggables when clicked again', async () => {
+ it('disables draggables when clicked again', async () => {
findRearrangeButton().vm.$emit('click');
await nextTick();
findRearrangeButton().vm.$emit('click');
await nextTick();
- expect(findRearrangeButton().attributes('pressed')).toBeFalsy();
+ expect(findRearrangeButton().attributes('pressed')).toBeUndefined();
expect(findEnabledDraggables().length).toBe(0);
});
});
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 721992e710a..3ccaa2d28ac 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -163,9 +163,6 @@ describe('DashboardsDropdown', () => {
findItemAt(1).vm.$emit('click');
});
- it('emits a "selectDashboard" event', () => {
- expect(wrapper.emitted().selectDashboard).toBeTruthy();
- });
it('emits a "selectDashboard" event with dashboard information', () => {
expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[0]]);
});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
index 755204dc721..b54ca926dae 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -68,7 +68,7 @@ describe('DuplicateDashboardForm', () => {
await nextTick();
expect(findByRef('fileNameFormGroup').classes()).toContain('is-invalid');
- expect(findInvalidFeedback().text()).toBeTruthy();
+ expect(findInvalidFeedback().text()).toBe('The file name should have a .yml extension');
});
});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
index 3032c236741..d83a9192876 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
@@ -72,7 +72,7 @@ describe('duplicate dashboard modal', () => {
await waitForPromises();
expect(okEvent.preventDefault).toHaveBeenCalled();
- expect(wrapper.emitted().dashboardDuplicated).toBeTruthy();
+ expect(wrapper.emitted('dashboardDuplicated')).toHaveLength(1);
expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index d1a13fbf9cd..a872a7780eb 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -855,7 +855,7 @@ describe('Monitoring store actions', () => {
);
});
- it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty ', () => {
+ it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty', () => {
mockMutate.mockResolvedValue({
data: {
project: null,
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index 1d6ea99155b..745707c1d28 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -30,9 +30,10 @@ describe('~/nav/components/top_nav_app.vue', () => {
it('renders nav item dropdown', () => {
expect(findNavItemDropdown().attributes('href')).toBeUndefined();
expect(findNavItemDropdown().attributes()).toMatchObject({
- icon: 'hamburger',
- text: TEST_NAV_DATA.activeTitle,
+ icon: '',
+ text: '',
'no-flip': '',
+ 'no-caret': '',
});
});
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 6cfbdb16111..048fca846ad 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -25,7 +25,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
};
const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
- const findMenuSections = () => wrapper.find(TopNavMenuSections);
+ const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js
index a7430d8c73f..b9cf39b8c1d 100644
--- a/spec/frontend/nav/components/top_nav_menu_item_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js
@@ -26,7 +26,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const findButtonIcons = () =>
findButton()
.findAllComponents(GlIcon)
diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
index d56542fe572..0ed5cffd93f 100644
--- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
@@ -4,11 +4,20 @@ import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
const TEST_SECTIONS = [
{
id: 'primary',
- menuItems: [{ id: 'test', href: '/test/href' }, { id: 'foo' }, { id: 'bar' }],
+ menuItems: [
+ { type: 'header', title: 'Heading' },
+ { type: 'item', id: 'test', href: '/test/href' },
+ { type: 'header', title: 'Another Heading' },
+ { type: 'item', id: 'foo' },
+ { type: 'item', id: 'bar' },
+ ],
},
{
id: 'secondary',
- menuItems: [{ id: 'lorem' }, { id: 'ipsum' }],
+ menuItems: [
+ { type: 'item', id: 'lorem' },
+ { type: 'item', id: 'ipsum' },
+ ],
},
];
@@ -25,10 +34,20 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
};
const findMenuItemModels = (parent) =>
- parent.findAll('[data-testid="menu-item"]').wrappers.map((x) => ({
- menuItem: x.props('menuItem'),
- classes: x.classes(),
- }));
+ parent.findAll('[data-testid="menu-header"],[data-testid="menu-item"]').wrappers.map((x) => {
+ return {
+ menuItem: x.vm
+ ? {
+ type: 'item',
+ ...x.props('menuItem'),
+ }
+ : {
+ type: 'header',
+ title: x.text(),
+ },
+ classes: x.classes(),
+ };
+ });
const findSectionModels = () =>
wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({
classes: x.classes(),
@@ -45,32 +64,31 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
});
it('renders sections with menu items', () => {
+ const headerClasses = ['gl-px-4', 'gl-py-2', 'gl-text-gray-900', 'gl-display-block'];
+ const itemClasses = ['gl-w-full'];
+
expect(findSectionModels()).toEqual([
{
classes: [],
- menuItems: [
- {
- menuItem: TEST_SECTIONS[0].menuItems[0],
- classes: ['gl-w-full'],
- },
- ...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({
+ menuItems: TEST_SECTIONS[0].menuItems.map((menuItem, index) => {
+ const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
+ if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
+ return {
menuItem,
- classes: ['gl-w-full', 'gl-mt-1'],
- })),
- ],
+ classes,
+ };
+ }),
},
{
classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
- menuItems: [
- {
- menuItem: TEST_SECTIONS[1].menuItems[0],
- classes: ['gl-w-full'],
- },
- ...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({
+ menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => {
+ const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
+ if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
+ return {
menuItem,
- classes: ['gl-w-full', 'gl-mt-1'],
- })),
- ],
+ classes,
+ };
+ }),
},
]);
});
@@ -88,7 +106,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItem.vm.$emit('click');
- expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[1]]]);
+ expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[3]]]);
});
});
diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js
index c2ad86a4605..2052acfe001 100644
--- a/spec/frontend/nav/mock_data.js
+++ b/spec/frontend/nav/mock_data.js
@@ -1,7 +1,7 @@
import { range } from 'lodash';
export const TEST_NAV_DATA = {
- activeTitle: 'Test Active Title',
+ menuTitle: 'Test Menu Title',
primary: [
...['projects', 'groups'].map((view) => ({
id: view,
diff --git a/spec/frontend/notebook/cells/output/latex_spec.js b/spec/frontend/notebook/cells/output/latex_spec.js
index 848d2069421..ed3b63be50a 100644
--- a/spec/frontend/notebook/cells/output/latex_spec.js
+++ b/spec/frontend/notebook/cells/output/latex_spec.js
@@ -27,7 +27,7 @@ describe('LaTeX output cell', () => {
${1} | ${false}
`('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => {
const wrapper = createComponent(inlineLatex, index);
- const prompt = wrapper.find(Prompt);
+ const prompt = wrapper.findComponent(Prompt);
expect(prompt.props().count).toEqual(count);
expect(prompt.props().showOutput).toEqual(expectation);
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index 475c41a72f6..b79000a3505 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -11,7 +11,7 @@ describe('Notebook component', () => {
function buildComponent(notebook) {
return mount(Component, {
- propsData: { notebook, codeCssClass: 'js-code-class' },
+ propsData: { notebook },
provide: { relativeRawPath: '' },
}).vm;
}
@@ -46,10 +46,6 @@ describe('Notebook component', () => {
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
-
- it('add code class to code blocks', () => {
- expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
- });
});
describe('with worksheets', () => {
@@ -72,9 +68,5 @@ describe('Notebook component', () => {
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
-
- it('add code class to code blocks', () => {
- expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
- });
});
});
diff --git a/spec/frontend/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js
deleted file mode 100644
index 944ccd6aa9f..00000000000
--- a/spec/frontend/notebook/lib/highlight_spec.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Prism from '~/notebook/lib/highlight';
-
-describe('Highlight library', () => {
- it('imports python language', () => {
- expect(Prism.languages.python).toBeDefined();
- });
-
- it('uses custom CSS classes', () => {
- const el = document.createElement('div');
- el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
-
- expect(el.querySelector('.string')).not.toBeNull();
- expect(el.querySelector('.function')).not.toBeNull();
- });
-});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 463787c148b..55e4ef42e37 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -586,10 +586,10 @@ describe('issue_comment_form component', () => {
${true}
${false}
`('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => {
- it(`sets \`confidential\` to \`${shouldCheckboxBeChecked}\``, async () => {
+ it(`sets \`internal\` to \`${shouldCheckboxBeChecked}\``, async () => {
mountComponent({
mountFunction: mount,
- initialData: { note: 'confidential note' },
+ initialData: { note: 'internal note' },
noteableData: { ...notableDataMockCanUpdateIssuable },
});
@@ -606,7 +606,7 @@ describe('issue_comment_form component', () => {
findCommentButton().trigger('click');
const [providedData] = wrapper.vm.saveNote.mock.calls[0];
- expect(providedData.data.note.confidential).toBe(shouldCheckboxBeChecked);
+ expect(providedData.data.note.internal).toBe(shouldCheckboxBeChecked);
});
});
@@ -679,7 +679,7 @@ describe('issue_comment_form component', () => {
);
});
- it('clicking `add comment now`, should call note endpoint, set `isDraft` false ', () => {
+ it('clicking `add comment now`, should call note endpoint, set `isDraft` false', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'a comment' } });
jest.spyOn(store, 'dispatch').mockResolvedValue();
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index a7e2f1efa09..f4ec7f835bb 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
@@ -45,7 +45,7 @@ describe('DiscussionCounter component', () => {
describe('has no discussions', () => {
it('does not render', () => {
- wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false);
});
@@ -55,7 +55,7 @@ describe('DiscussionCounter component', () => {
it('does not render', () => {
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts');
- wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false);
});
@@ -75,7 +75,7 @@ describe('DiscussionCounter component', () => {
it('renders', () => {
updateStore();
- wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(true);
});
@@ -89,7 +89,7 @@ describe('DiscussionCounter component', () => {
({ blocksMerge, color }) => {
updateStore();
store.state.unresolvedDiscussionsCount = 1;
- wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge } });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge } });
expect(wrapper.find('[data-testid="discussions-counter-text"]').classes()).toContain(color);
},
@@ -97,60 +97,58 @@ describe('DiscussionCounter component', () => {
it.each`
title | resolved | groupLength
- ${'not allResolved'} | ${false} | ${4}
+ ${'not allResolved'} | ${false} | ${2}
${'allResolved'} | ${true} | ${1}
- `('renders correctly if $title', ({ resolved, groupLength }) => {
+ `('renders correctly if $title', async ({ resolved, groupLength }) => {
updateStore({ resolvable: true, resolved });
- wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+ await wrapper.find('.dropdown-toggle').trigger('click');
- expect(wrapper.findAllComponents(GlButton)).toHaveLength(groupLength);
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(groupLength);
});
});
describe('toggle all threads button', () => {
let toggleAllButton;
- const updateStoreWithExpanded = (expanded) => {
+ const updateStoreWithExpanded = async (expanded) => {
const discussion = { ...discussionMock, expanded };
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
- wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+ await wrapper.find('.dropdown-toggle').trigger('click');
+ toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]');
};
afterEach(() => wrapper.destroy());
- it('calls button handler when clicked', () => {
- updateStoreWithExpanded(true);
+ it('calls button handler when clicked', async () => {
+ await updateStoreWithExpanded(true);
- toggleAllButton.vm.$emit('click');
+ toggleAllButton.trigger('click');
expect(setExpandDiscussionsFn).toHaveBeenCalledTimes(1);
});
it('collapses all discussions if expanded', async () => {
- updateStoreWithExpanded(true);
+ await updateStoreWithExpanded(true);
expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.props('icon')).toBe('collapse');
- toggleAllButton.vm.$emit('click');
+ toggleAllButton.trigger('click');
await nextTick();
expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.props('icon')).toBe('expand');
});
it('expands all discussions if collapsed', async () => {
- updateStoreWithExpanded(false);
+ await updateStoreWithExpanded(false);
expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.props('icon')).toBe('expand');
- toggleAllButton.vm.$emit('click');
+ toggleAllButton.trigger('click');
await nextTick();
expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.props('icon')).toBe('collapse');
});
});
});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index 27206bddbfc..ed9fc47540d 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -8,7 +8,14 @@ import createEventHub from '~/helpers/event_hub_factory';
import axios from '~/lib/utils/axios_utils';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
-import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import Tracking from '~/tracking';
+import {
+ DISCUSSION_FILTERS_DEFAULT_VALUE,
+ DISCUSSION_FILTER_TYPES,
+ ASC,
+ DESC,
+} from '~/notes/constants';
import notesModule from '~/notes/stores/modules';
import { discussionFiltersMock, discussionMock } from '../mock_data';
@@ -28,6 +35,8 @@ describe('DiscussionFilter component', () => {
const findFilter = (filterType) =>
wrapper.find(`.dropdown-item[data-filter-type="${filterType}"]`);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+
const mountComponent = () => {
const discussions = [
{
@@ -68,6 +77,7 @@ describe('DiscussionFilter component', () => {
mock.onGet(DISCUSSION_PATH).reply(200, '');
window.mrTabs = undefined;
wrapper = mountComponent();
+ jest.spyOn(Tracking, 'event');
});
afterEach(() => {
@@ -75,6 +85,65 @@ describe('DiscussionFilter component', () => {
mock.restore();
});
+ describe('default', () => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ });
+
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
+ });
+
+ it('calls setDiscussionSortDirection when update is emitted', () => {
+ findLocalStorageSync().vm.$emit('input', ASC);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { direction: ASC });
+ });
+ });
+
+ describe('when asc', () => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ });
+
+ describe('when the dropdown is clicked', () => {
+ it('calls the right actions', () => {
+ wrapper.find('.js-newest-first').vm.$emit('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
+ direction: DESC,
+ });
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
+ property: DESC,
+ });
+ });
+ });
+ });
+
+ describe('when desc', () => {
+ beforeEach(() => {
+ store.state.discussionSortOrder = DESC;
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ });
+
+ describe('when the dropdown item is clicked', () => {
+ it('calls the right actions', () => {
+ wrapper.find('.js-oldest-first').vm.$emit('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
+ direction: ASC,
+ });
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
+ property: ASC,
+ });
+ });
+
+ it('sets is-checked to true on the active button in the dropdown', () => {
+ expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true);
+ });
+ });
+ });
+
it('renders the all filters', () => {
expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe(
discussionFiltersMock.length,
@@ -82,7 +151,7 @@ describe('DiscussionFilter component', () => {
});
it('renders the default selected item', () => {
- expect(wrapper.find('#discussion-filter-dropdown .dropdown-item').text().trim()).toBe(
+ expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe(
discussionFiltersMock[0].title,
);
});
@@ -127,14 +196,6 @@ describe('DiscussionFilter component', () => {
expect(wrapper.vm.$store.state.commentsDisabled).toBe(false);
});
- it('renders a dropdown divider for the default filter', () => {
- const defaultFilter = wrapper.findAll(
- `.discussion-filter-container .dropdown-item-wrapper > *`,
- );
-
- expect(defaultFilter.at(1).classes('gl-new-dropdown-divider')).toBe(true);
- });
-
describe('Merge request tabs', () => {
eventHub = createEventHub();
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 1b8b6bec490..a74d709ed3a 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -140,21 +140,21 @@ describe('DiscussionNotes', () => {
findNoteAtIndex(0).vm.$emit('handleDeleteNote');
await nextTick();
- expect(wrapper.emitted().deleteNote).toBeTruthy();
+ expect(wrapper.emitted().deleteNote).toHaveLength(1);
});
it('emits startReplying when first note emits startReplying', async () => {
findNoteAtIndex(0).vm.$emit('startReplying');
await nextTick();
- expect(wrapper.emitted().startReplying).toBeTruthy();
+ expect(wrapper.emitted().startReplying).toHaveLength(1);
});
it('emits deleteNote when second note emits handleDeleteNote', async () => {
findNoteAtIndex(1).vm.$emit('handleDeleteNote');
await nextTick();
- expect(wrapper.emitted().deleteNote).toBeTruthy();
+ expect(wrapper.emitted().deleteNote).toHaveLength(1);
});
});
@@ -169,7 +169,7 @@ describe('DiscussionNotes', () => {
note.vm.$emit('handleDeleteNote');
await nextTick();
- expect(wrapper.emitted().deleteNote).toBeTruthy();
+ expect(wrapper.emitted().deleteNote).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
index 71406eeb7b4..a185f11ffaa 100644
--- a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
@@ -19,7 +19,7 @@ describe('ResolveWithIssueButton', () => {
wrapper.destroy();
});
- it('it should have a link with the provided link property as href', () => {
+ it('should have a link with the provided link property as href', () => {
const button = wrapper.findComponent(GlButton);
expect(button.attributes().href).toBe(url);
diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js
index b027a261c15..8446bba340f 100644
--- a/spec/frontend/notes/components/multiline_comment_form_spec.js
+++ b/spec/frontend/notes/components/multiline_comment_form_spec.js
@@ -70,7 +70,7 @@ describe('MultilineCommentForm', () => {
glSelect.vm.$emit('change', { ...testLine });
expect(wrapper.vm.commentLineStart).toEqual(line);
- expect(wrapper.emitted('input')).toBeTruthy();
+ expect(wrapper.emitted('input')).toHaveLength(1);
// Once during created, once during updateCommentLineStart
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
});
diff --git a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
new file mode 100644
index 00000000000..658e844a9b1
--- /dev/null
+++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
@@ -0,0 +1,35 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
+
+const emitData = {
+ noteId: '1',
+ addError: 'Error promoting the note to timeline event: %{error}',
+ addGenericError: 'Something went wrong while promoting the note to timeline event.',
+};
+
+describe('NoteTimelineEventButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(TimelineEventButton, {
+ propsData: {
+ noteId: '1',
+ isPromotionInProgress: true,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTimelineButton = () => wrapper.findComponent(GlButton);
+
+ it('emits click-promote-comment-to-event', async () => {
+ findTimelineButton().vm.$emit('click');
+
+ expect(wrapper.emitted('click-promote-comment-to-event')).toEqual([[emitData]]);
+ expect(findTimelineButton().props('disabled')).toEqual(true);
+ });
+});
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index c2e56d3e7a7..3b5313744ff 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -74,11 +74,11 @@ describe('issue_note_body component', () => {
});
it.each`
- confidential | buttonText
- ${false} | ${'Save comment'}
- ${true} | ${'Save internal note'}
- `('renders save button with text "$buttonText"', ({ confidential, buttonText }) => {
- wrapper = createComponent({ props: { note: { ...note, confidential }, isEditing: true } });
+ internal | buttonText
+ ${false} | ${'Save comment'}
+ ${true} | ${'Save internal note'}
+ `('renders save button with text "$buttonText"', ({ internal, buttonText }) => {
+ wrapper = createComponent({ props: { note: { ...note, internal }, isEditing: true } });
expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index fad04e9063d..90473e7ccba 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -116,15 +116,15 @@ describe('issue_note_form component', () => {
});
it.each`
- confidential | placeholder
- ${false} | ${'Write a comment or drag your files here…'}
- ${true} | ${'Write an internal note or drag your files here…'}
+ internal | placeholder
+ ${false} | ${'Write a comment or drag your files here…'}
+ ${true} | ${'Write an internal note or drag your files here…'}
`(
- 'should set correct textarea placeholder text when discussion confidentiality is $confidential',
- ({ confidential, placeholder }) => {
+ 'should set correct textarea placeholder text when discussion confidentiality is $internal',
+ ({ internal, placeholder }) => {
props.note = {
...note,
- confidential,
+ internal,
};
wrapper = createComponentWrapper();
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 43fbc5e26dc..76177229cff 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -3,7 +3,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NoteHeader from '~/notes/components/note_header.vue';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
Vue.use(Vuex);
@@ -40,13 +40,19 @@ describe('NoteHeader component', () => {
availability: '',
};
- const createComponent = (props) => {
+ const createComponent = (props, userAttributes = false) => {
wrapper = shallowMountExtended(NoteHeader, {
store: new Vuex.Store({
actions,
}),
propsData: { ...props },
stubs: { GlSprintf, UserNameWithStatus },
+ provide: {
+ glFeatures: {
+ removeUserAttributesProjects: userAttributes,
+ removeUserAttributesGroups: userAttributes,
+ },
+ },
});
};
@@ -55,6 +61,26 @@ describe('NoteHeader component', () => {
wrapper = null;
});
+ describe('when removeUserAttributesProjects feature flag is enabled', () => {
+ it('does not render busy status', () => {
+ createComponent({ author: { ...author, availability: AVAILABILITY_STATUS.BUSY } }, true);
+
+ expect(wrapper.find('.note-header-info').text()).not.toContain('(Busy)');
+ });
+
+ it('does not render author status', () => {
+ createComponent({ author }, true);
+
+ expect(findAuthorStatus().exists()).toBe(false);
+ });
+
+ it('does not render username', () => {
+ createComponent({ author }, true);
+
+ expect(wrapper.find('.note-header-info').text()).not.toContain('@');
+ });
+ });
+
it('does not render discussion actions when includeToggle is false', () => {
createComponent({
includeToggle: false,
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index b34305688d9..2175849aeb9 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -97,7 +97,7 @@ describe('noteable_discussion component', () => {
`(
'reply button on form should have title "$saveButtonTitle" when note is $noteType',
async ({ isNoteInternal, saveButtonTitle }) => {
- wrapper.setProps({ discussion: { ...discussionMock, confidential: isNoteInternal } });
+ wrapper.setProps({ discussion: { ...discussionMock, internal: isNoteInternal } });
await nextTick();
const replyPlaceholder = wrapper.findComponent(ReplyPlaceholder);
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index e049c5bc0c8..b044d40cbe4 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -292,7 +292,7 @@ describe('issue_note', () => {
describe('internal note', () => {
it('has internal note class for internal notes', () => {
- createWrapper({ note: { ...note, confidential: true } });
+ createWrapper({ note: { ...note, internal: true } });
expect(wrapper.classes()).toContain('internal-note');
});
diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js
deleted file mode 100644
index 8b6e05da3c0..00000000000
--- a/spec/frontend/notes/components/sort_discussion_spec.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import SortDiscussion from '~/notes/components/sort_discussion.vue';
-import { ASC, DESC } from '~/notes/constants';
-import createStore from '~/notes/stores';
-import Tracking from '~/tracking';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-Vue.use(Vuex);
-
-describe('Sort Discussion component', () => {
- let wrapper;
- let store;
-
- const createComponent = () => {
- jest.spyOn(store, 'dispatch').mockImplementation();
-
- wrapper = shallowMount(SortDiscussion, {
- store,
- });
- };
-
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(Tracking, 'event');
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('has local storage sync with the correct props', () => {
- expect(findLocalStorageSync().props('asString')).toBe(true);
- });
-
- it('calls setDiscussionSortDirection when update is emitted', () => {
- findLocalStorageSync().vm.$emit('input', ASC);
-
- expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { direction: ASC });
- });
- });
-
- describe('when asc', () => {
- describe('when the dropdown is clicked', () => {
- it('calls the right actions', () => {
- createComponent();
-
- wrapper.find('.js-newest-first').vm.$emit('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
- direction: DESC,
- });
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
- property: DESC,
- });
- });
- });
-
- it('shows the "Oldest First" as the dropdown', () => {
- createComponent();
-
- expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Oldest first');
- });
- });
-
- describe('when desc', () => {
- beforeEach(() => {
- store.state.discussionSortOrder = DESC;
- createComponent();
- });
-
- describe('when the dropdown item is clicked', () => {
- it('calls the right actions', () => {
- wrapper.find('.js-oldest-first').vm.$emit('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
- direction: ASC,
- });
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
- property: ASC,
- });
- });
-
- it('sets is-checked to true on the active button in the dropdown', () => {
- expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true);
- });
- });
-
- it('shows the "Newest First" as the dropdown', () => {
- expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Newest first');
- });
- });
-});
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 35b3dec6298..1b4e8026d84 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -110,16 +110,13 @@ describe('Discussion navigation mixin', () => {
});
describe.each`
- fn | args | currentId | expected
- ${'jumpToNextDiscussion'} | ${[]} | ${null} | ${'a'}
- ${'jumpToNextDiscussion'} | ${[]} | ${'a'} | ${'c'}
- ${'jumpToNextDiscussion'} | ${[]} | ${'e'} | ${'a'}
- ${'jumpToPreviousDiscussion'} | ${[]} | ${null} | ${'e'}
- ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'} | ${'c'}
- ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'} | ${'a'}
- ${'jumpToNextRelativeDiscussion'} | ${[null]} | ${null} | ${'a'}
- ${'jumpToNextRelativeDiscussion'} | ${['a']} | ${null} | ${'c'}
- ${'jumpToNextRelativeDiscussion'} | ${['e']} | ${'c'} | ${'a'}
+ fn | args | currentId | expected
+ ${'jumpToNextDiscussion'} | ${[]} | ${null} | ${'a'}
+ ${'jumpToNextDiscussion'} | ${[]} | ${'a'} | ${'c'}
+ ${'jumpToNextDiscussion'} | ${[]} | ${'e'} | ${'a'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${null} | ${'e'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'} | ${'c'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'} | ${'a'}
`('$fn (args = $args, currentId = $currentId)', ({ fn, args, currentId, expected }) => {
beforeEach(() => {
store.state.notes.currentDiscussionId = currentId;
@@ -133,19 +130,12 @@ describe('Discussion navigation mixin', () => {
await nextTick();
});
- it('sets current discussion', () => {
- expect(store.state.notes.currentDiscussionId).toEqual(expected);
- });
-
it('expands discussion', () => {
expect(expandDiscussion).toHaveBeenCalled();
});
it('scrolls to element', () => {
- expect(utils.scrollToElement).toHaveBeenCalledWith(
- findDiscussion('div.discussion', expected),
- { behavior: 'auto' },
- );
+ expect(utils.scrollToElement).toHaveBeenCalled();
});
});
@@ -172,7 +162,7 @@ describe('Discussion navigation mixin', () => {
expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
findDiscussion('ul.notes', expected),
- { behavior: 'auto' },
+ { behavior: 'auto', offset: 0 },
);
});
});
@@ -213,7 +203,7 @@ describe('Discussion navigation mixin', () => {
it('scrolls to discussion', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
- { behavior: 'auto' },
+ { behavior: 'auto', offset: 0 },
);
});
});
@@ -244,7 +234,6 @@ describe('Discussion navigation mixin', () => {
it.each`
tabValue
${'diffs'}
- ${'show'}
${'other'}
`(
'calls scrollToFile with setHash as $hashValue when the tab is $tabValue',
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 02b27eca196..989dd74b6d0 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
import createFlash from '~/flash';
+import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import * as notesConstants from '~/notes/constants';
@@ -14,7 +15,9 @@ import mutations from '~/notes/stores/mutations';
import * as utils from '~/notes/stores/utils';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import promoteTimelineEvent from '~/notes/graphql/promote_timeline_event.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
+import notesEventHub from '~/notes/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
import { resetStore } from '../helpers';
import {
@@ -38,6 +41,8 @@ jest.mock('~/flash', () => {
return flash;
});
+jest.mock('~/vue_shared/plugins/global_toast');
+
describe('Actions Notes Store', () => {
let commit;
let dispatch;
@@ -1324,6 +1329,102 @@ describe('Actions Notes Store', () => {
});
});
+ describe('promoteCommentToTimelineEvent', () => {
+ const actionArgs = {
+ noteId: '1',
+ addError: 'addError: Create error',
+ addGenericError: 'addGenericError',
+ };
+ const commitSpy = jest.fn();
+
+ describe('for successful request', () => {
+ const timelineEventSuccessResponse = {
+ data: {
+ timelineEventPromoteFromNote: {
+ timelineEvent: {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/19',
+ },
+ errors: [],
+ },
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(timelineEventSuccessResponse);
+ });
+
+ it('calls gqClient mutation with the correct values', () => {
+ actions.promoteCommentToTimelineEvent({ commit: () => {} }, actionArgs);
+
+ expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
+ expect(utils.gqClient.mutate).toHaveBeenCalledWith({
+ mutation: promoteTimelineEvent,
+ variables: {
+ input: {
+ noteId: 'gid://gitlab/Note/1',
+ },
+ },
+ });
+ });
+
+ it('returns success response', () => {
+ jest.spyOn(notesEventHub, '$emit').mockImplementation(() => {});
+
+ return actions.promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs).then(() => {
+ expect(notesEventHub.$emit).toHaveBeenLastCalledWith(
+ 'comment-promoted-to-timeline-event',
+ );
+ expect(toast).toHaveBeenCalledWith('Comment added to the timeline.');
+ expect(commitSpy).toHaveBeenCalledWith(
+ mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS,
+ false,
+ );
+ });
+ });
+ });
+
+ describe('for failing request', () => {
+ const errorResponse = {
+ data: {
+ timelineEventPromoteFromNote: {
+ timelineEvent: null,
+ errors: ['Create error'],
+ },
+ },
+ };
+
+ it.each`
+ mockReject | message | captureError | error
+ ${true} | ${'addGenericError'} | ${true} | ${new Error()}
+ ${false} | ${'addError: Create error'} | ${false} | ${null}
+ `(
+ 'should show an error when submission fails',
+ ({ mockReject, message, captureError, error }) => {
+ const expectedAlertArgs = {
+ captureError,
+ error,
+ message,
+ };
+ if (mockReject) {
+ jest.spyOn(utils.gqClient, 'mutate').mockRejectedValueOnce(new Error());
+ } else {
+ jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(errorResponse);
+ }
+
+ return actions
+ .promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs)
+ .then(() => {
+ expect(createFlash).toHaveBeenCalledWith(expectedAlertArgs);
+ expect(commitSpy).toHaveBeenCalledWith(
+ mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS,
+ false,
+ );
+ });
+ },
+ );
+ });
+ });
+
describe('setFetchingState', () => {
it('commits SET_NOTES_FETCHING_STATE', () => {
return testAction(
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index 6d078dcefcf..e03fa854e54 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -211,7 +211,7 @@ describe('Getters Notes Store', () => {
describe('isNotesFetched', () => {
it('should return the state for the fetching notes', () => {
- expect(getters.isNotesFetched(state)).toBeFalsy();
+ expect(getters.isNotesFetched(state)).toBe(false);
});
});
@@ -512,8 +512,8 @@ describe('Getters Notes Store', () => {
unresolvedDiscussionsIdsByDate: [],
};
- expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeFalsy();
- expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy();
+ expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeUndefined();
+ expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeUndefined();
});
});
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index e0a0fc43ffe..8809a496c52 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -74,7 +74,7 @@ describe('Notes Store mutations', () => {
});
describe('DELETE_NOTE', () => {
- it('should delete a note ', () => {
+ it('should delete a note', () => {
const state = { discussions: [discussionMock] };
const toDelete = discussionMock.notes[0];
const lengthBefore = discussionMock.notes.length;
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index c5d201c3aec..cd04adac72d 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -56,8 +56,8 @@ describe('CustomNotificationsModal', () => {
);
}
- const findModalBodyDescription = () => wrapper.find(GlSprintf);
- const findAllCheckboxes = () => wrapper.findAll(GlFormCheckbox);
+ const findModalBodyDescription = () => wrapper.findComponent(GlSprintf);
+ const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
const findCheckboxAt = (index) => findAllCheckboxes().at(index);
beforeEach(() => {
@@ -111,7 +111,7 @@ describe('CustomNotificationsModal', () => {
const checkbox = findCheckboxAt(index);
expect(checkbox.text()).toContain(eventName);
expect(checkbox.vm.$attrs.checked).toBe(enabled);
- expect(checkbox.find(GlLoadingIcon).exists()).toBe(loading);
+ expect(checkbox.findComponent(GlLoadingIcon).exists()).toBe(loading);
},
);
});
@@ -142,7 +142,7 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent({ injectedProperties });
- wrapper.find(GlModal).vm.$emit('show');
+ wrapper.findComponent(GlModal).vm.$emit('show');
await waitForPromises();
@@ -159,7 +159,7 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent();
- wrapper.find(GlModal).vm.$emit('show');
+ wrapper.findComponent(GlModal).vm.$emit('show');
expect(wrapper.vm.isLoading).toBe(true);
await waitForPromises();
@@ -176,7 +176,7 @@ describe('CustomNotificationsModal', () => {
mockAxios.onGet('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent();
- wrapper.find(GlModal).vm.$emit('show');
+ wrapper.findComponent(GlModal).vm.$emit('show');
await waitForPromises();
@@ -197,7 +197,7 @@ describe('CustomNotificationsModal', () => {
${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group'} | ${'a groupId is given'}
${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global'} | ${'neither projectId nor groupId are given'}
`(
- 'updates the $notificationType notification settings when $condition and the user clicks the checkbox ',
+ 'updates the $notificationType notification settings when $condition and the user clicks the checkbox',
async ({ projectId, groupId, endpointUrl }) => {
mockAxios
.onGet(endpointUrl)
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index 7ca6c2052ae..7a98b374095 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -40,12 +40,13 @@ describe('NotificationsDropdown', () => {
});
}
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
+ const findAllNotificationsDropdownItems = () =>
+ wrapper.findAllComponents(NotificationsDropdownItem);
const findDropdownItemAt = (index) =>
- findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
- const findNotificationsModal = () => wrapper.find(CustomNotificationsModal);
+ findAllNotificationsDropdownItems().at(index).findComponent(GlDropdownItem);
+ const findNotificationsModal = () => wrapper.findComponent(CustomNotificationsModal);
const clickDropdownItemAt = async (index) => {
const dropdownItem = findDropdownItemAt(index);
@@ -243,7 +244,7 @@ describe('NotificationsDropdown', () => {
expect(dropdownItem.props('isChecked')).toBe(true);
});
- it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => {
+ it("won't update the selectedNotificationLevel and shows a toast message when the request fails and", async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent();
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 21145466016..810049220ae 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -63,7 +63,7 @@ describe('operation settings external dashboard component', () => {
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
mountComponent();
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.text()).toBe('Expand');
});
@@ -82,7 +82,7 @@ describe('operation settings external dashboard component', () => {
});
it('renders help page link', () => {
- const link = subHeader.find(GlLink);
+ const link = subHeader.findComponent(GlLink);
expect(link.text()).toBe('Learn more.');
expect(link.attributes().href).toBe(helpPage);
@@ -96,7 +96,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent(false);
- formGroup = wrapper.find(DashboardTimezone).find(GlFormGroup);
+ formGroup = wrapper.findComponent(DashboardTimezone).findComponent(GlFormGroup);
});
it('uses label text', () => {
@@ -117,7 +117,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent();
- select = wrapper.find(DashboardTimezone).find(GlFormSelect);
+ select = wrapper.findComponent(DashboardTimezone).findComponent(GlFormSelect);
});
it('defaults to externalDashboardUrl', () => {
@@ -132,7 +132,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent(false);
- formGroup = wrapper.find(ExternalDashboard).find(GlFormGroup);
+ formGroup = wrapper.findComponent(ExternalDashboard).findComponent(GlFormGroup);
});
it('uses label text', () => {
@@ -153,7 +153,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent();
- input = wrapper.find(ExternalDashboard).find(GlFormInput);
+ input = wrapper.findComponent(ExternalDashboard).findComponent(GlFormInput);
});
it('defaults to externalDashboardUrl', () => {
@@ -167,7 +167,7 @@ describe('operation settings external dashboard component', () => {
});
describe('submit button', () => {
- const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+ const findSubmitButton = () => wrapper.find('.settings-content form').findComponent(GlButton);
const endpointRequest = [
operationsSettingsEndpoint,
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
index ad67128502a..ff11c8843bb 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
@@ -11,8 +11,8 @@ describe('delete_button', () => {
tooltipTitle: 'Bar tooltipTitle',
};
- const findButton = () => wrapper.find(GlButton);
- const findTooltip = () => wrapper.find(GlTooltip);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
const mountComponent = (props) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index 9680e273add..4a026f35822 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -13,8 +13,8 @@ import {
describe('Delete alert', () => {
let wrapper;
- const findAlert = () => wrapper.find(GlAlert);
- const findLink = () => wrapper.find(GlLink);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
const mountComponent = (propsData) => {
wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index 9982286c625..b37edac83f7 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -120,7 +120,7 @@ describe('Details Header', () => {
return waitForPromises();
});
- it('shows image.name ', () => {
+ it('shows image.name', () => {
expect(findTitle().text()).toContain('foo');
});
@@ -289,7 +289,7 @@ describe('Details Header', () => {
);
});
- describe('visibility and updated at ', () => {
+ describe('visibility and updated at', () => {
it('has last updated text', async () => {
mountComponent();
await waitForMetadataItems();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
index 1a27481a828..ce5ecfe4608 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
@@ -9,7 +9,7 @@ import {
describe('Partial Cleanup alert', () => {
let wrapper;
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findRunLink = () => wrapper.find('[data-testid="run-link"');
const findHelpLink = () => wrapper.find('[data-testid="help-link"');
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
index a11b102d9a6..d83a5099bcd 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
@@ -14,8 +14,8 @@ import {
describe('Status Alert', () => {
let wrapper;
- const findLink = () => wrapper.find(GlLink);
- const findAlert = () => wrapper.find(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findMessage = () => wrapper.find('[data-testid="message"]');
const mountComponent = (propsData) => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index 84f01f10f21..96c670eaad2 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -32,7 +32,7 @@ describe('tags list row', () => {
const findShortRevision = () => wrapper.find('[data-testid="digest"]');
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
- const findDetailsRows = () => wrapper.findAll(DetailsRow);
+ const findDetailsRows = () => wrapper.findAllComponents(DetailsRow);
const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]');
const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]');
const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]');
@@ -359,7 +359,7 @@ describe('tags list row', () => {
mountComponent();
await nextTick();
- expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard);
+ expect(finderFunction().findComponent(ClipboardButton).exists()).toBe(clipboard);
});
it('is disabled when the component is disabled', async () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
index e5df260a260..88e79c513bc 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
@@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '../../stubs';
describe('TagsLoader component', () => {
let wrapper;
- const findGlSkeletonLoaders = () => wrapper.findAll(GlSkeletonLoader);
+ const findGlSkeletonLoaders = () => wrapper.findAllComponents(GlSkeletonLoader);
const mountComponent = () => {
wrapper = shallowMount(component, {
@@ -25,7 +25,7 @@ describe('TagsLoader component', () => {
wrapper = null;
});
- it('produces the correct amount of loaders ', () => {
+ it('produces the correct amount of loaders', () => {
mountComponent();
expect(findGlSkeletonLoaders().length).toBe(1);
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index 61503d0f3bf..535faebdd4e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -16,7 +16,7 @@ describe('cleanup_status', () => {
let wrapper;
const findMainIcon = () => wrapper.findByTestId('main-icon');
- const findMainIconName = () => wrapper.findByTestId('main-icon').find(GlIcon);
+ const findMainIconName = () => wrapper.findByTestId('main-icon').findComponent(GlIcon);
const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
const findPopover = () => wrapper.findComponent(GlPopover);
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index d12933526bc..0b59fe2d8ce 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -233,7 +233,7 @@ describe('Image List Row', () => {
it('contains a tag icon', () => {
mountComponent();
- const icon = findTagsCount().find(GlIcon);
+ const icon = findTagsCount().findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('tag');
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
index e0119954ed4..042b8383571 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
@@ -8,8 +8,8 @@ import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data
describe('Image List', () => {
let wrapper;
- const findRow = () => wrapper.findAll(ImageListRow);
- const findPagination = () => wrapper.find(GlKeysetPagination);
+ const findRow = () => wrapper.findAllComponents(ImageListRow);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const mountComponent = (props) => {
wrapper = shallowMount(Component, {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index a006de9f00c..e6d81d4a28f 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -17,7 +17,7 @@ jest.mock('~/lib/utils/datetime_utility', () => ({
describe('registry_header', () => {
let wrapper;
- const findTitleArea = () => wrapper.find(TitleArea);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 1d161888a4d..ee6470a9df8 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -45,16 +45,16 @@ describe('Details Page', () => {
let wrapper;
let apolloProvider;
- const findDeleteModal = () => wrapper.find(DeleteModal);
- const findPagination = () => wrapper.find(GlKeysetPagination);
- const findTagsLoader = () => wrapper.find(TagsLoader);
- const findTagsList = () => wrapper.find(TagsList);
- const findDeleteAlert = () => wrapper.find(DeleteAlert);
- const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findEmptyState = () => wrapper.find(GlEmptyState);
- const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
- const findStatusAlert = () => wrapper.find(StatusAlert);
- const findDeleteImage = () => wrapper.find(DeleteImage);
+ const findDeleteModal = () => wrapper.findComponent(DeleteModal);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const findTagsList = () => wrapper.findComponent(TagsList);
+ const findDeleteAlert = () => wrapper.findComponent(DeleteAlert);
+ const findDetailsHeader = () => wrapper.findComponent(DetailsHeader);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findPartialCleanupAlert = () => wrapper.findComponent(PartialCleanupAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
+ const findDeleteImage = () => wrapper.findComponent(DeleteImage);
const routeId = 1;
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
index 5f4cb8969bc..add772d27ef 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
@@ -4,7 +4,7 @@ import component from '~/packages_and_registries/container_registry/explorer/pag
describe('List Page', () => {
let wrapper;
- const findRouterView = () => wrapper.find({ ref: 'router-view' });
+ const findRouterView = () => wrapper.findComponent({ ref: 'router-view' });
const mountComponent = () => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js
new file mode 100644
index 00000000000..a2e5cbdce8b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js
@@ -0,0 +1,143 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { n__ } from '~/locale';
+import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue';
+import RealListItem from '~/vue_shared/components/registry/list_item.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { harborArtifactsList, defaultConfig } from '../../mock_data';
+
+describe('Harbor artifact list row', () => {
+ let wrapper;
+
+ const ListItem = {
+ ...RealListItem,
+ data() {
+ return {
+ detailsSlots: [],
+ isDetailsShown: true,
+ };
+ },
+ };
+
+ const RouterLinkStub = {
+ props: {
+ to: {
+ type: Object,
+ },
+ },
+ render(createElement) {
+ return createElement('a', {}, this.$slots.default);
+ },
+ };
+
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findClipboardButton = () => wrapper.findAllComponents(ClipboardButton);
+ const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
+ const findByTestId = (testId) => wrapper.findByTestId(testId);
+
+ const $route = {
+ params: {
+ project: defaultConfig.harborIntegrationProjectName,
+ image: 'test-repository',
+ },
+ };
+
+ const mountComponent = ({ propsData, config = defaultConfig }) => {
+ wrapper = shallowMountExtended(ArtifactsListRow, {
+ stubs: {
+ GlSprintf,
+ ListItem,
+ 'router-link': RouterLinkStub,
+ },
+ mocks: {
+ $route,
+ },
+ propsData,
+ provide() {
+ return {
+ ...config,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('list item', () => {
+ beforeEach(() => {
+ mountComponent({
+ propsData: {
+ artifact: harborArtifactsList[0],
+ },
+ });
+ });
+
+ it('exists', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ it('has the correct artifact name', () => {
+ expect(findByTestId('name').text()).toBe(harborArtifactsList[0].digest);
+ });
+
+ it('has the correct tags count', () => {
+ const tagsCount = harborArtifactsList[0].tags.length;
+ expect(findByTestId('tags-count').text()).toBe(n__('%d tag', '%d tags', tagsCount));
+ });
+
+ it('has correct digest', () => {
+ expect(findByTestId('digest').text()).toBe('Digest: mock_sh');
+ });
+ describe('time', () => {
+ it('has the correct push time', () => {
+ expect(findByTestId('time').text()).toBe('Published');
+ expect(findTimeAgoTooltip().attributes()).toMatchObject({
+ time: harborArtifactsList[0].pushTime,
+ });
+ });
+ });
+
+ describe('clipboard button', () => {
+ it('exists', () => {
+ expect(findClipboardButton()).toHaveLength(2);
+ });
+
+ it('has the correct props', () => {
+ expect(findClipboardButton().at(0).attributes()).toMatchObject({
+ text: `docker pull demo.harbor.com/test-project/test-repository@${harborArtifactsList[0].digest}`,
+ title: `docker pull demo.harbor.com/test-project/test-repository@${harborArtifactsList[0].digest}`,
+ });
+
+ expect(findClipboardButton().at(1).attributes()).toMatchObject({
+ text: harborArtifactsList[0].digest,
+ title: harborArtifactsList[0].digest,
+ });
+ });
+ });
+
+ describe('size', () => {
+ it('calculated correctly', () => {
+ expect(findByTestId('size').text()).toBe(
+ numberToHumanSize(Number(harborArtifactsList[0].size)),
+ );
+ });
+
+ it('when size is missing', () => {
+ const artifactInfo = harborArtifactsList[0];
+ artifactInfo.size = null;
+
+ mountComponent({
+ propsData: {
+ artifact: artifactInfo,
+ },
+ });
+
+ expect(findByTestId('size').text()).toBe('0 bytes');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
new file mode 100644
index 00000000000..b9d6dc2679e
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
@@ -0,0 +1,75 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState } from '@gitlab/ui';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue';
+import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue';
+import { defaultConfig, harborArtifactsList } from '../../mock_data';
+
+describe('Harbor artifacts list', () => {
+ let wrapper;
+
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findRegistryList = () => wrapper.findComponent(RegistryList);
+ const findArtifactsListRow = () => wrapper.findAllComponents(ArtifactsListRow);
+
+ const mountComponent = ({ propsData, config = defaultConfig }) => {
+ wrapper = shallowMount(ArtifactsList, {
+ propsData,
+ stubs: { RegistryList },
+ provide() {
+ return {
+ ...config,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when isLoading is true', () => {
+ beforeEach(() => {
+ mountComponent({
+ propsData: {
+ isLoading: true,
+ pageInfo: {},
+ filter: '',
+ artifacts: [],
+ },
+ });
+ });
+
+ it('show the loader', () => {
+ expect(findTagsLoader().exists()).toBe(true);
+ });
+
+ it('does not show the list', () => {
+ expect(findGlEmptyState().exists()).toBe(false);
+ expect(findRegistryList().exists()).toBe(false);
+ });
+ });
+
+ describe('registry list', () => {
+ beforeEach(() => {
+ mountComponent({
+ propsData: {
+ isLoading: false,
+ pageInfo: {},
+ filter: '',
+ artifacts: harborArtifactsList,
+ },
+ });
+ });
+
+ it('exists', () => {
+ expect(findRegistryList().exists()).toBe(true);
+ });
+
+ it('one artifact row exist', () => {
+ expect(findArtifactsListRow()).toHaveLength(harborArtifactsList.length);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js
new file mode 100644
index 00000000000..e8cc2b2e22d
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js
@@ -0,0 +1,85 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { ROOT_IMAGE_TEXT } from '~/packages_and_registries/harbor_registry/constants/index';
+
+describe('Harbor Details Header', () => {
+ let wrapper;
+
+ const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
+ const findTitle = () => findByTestId('title');
+ const findArtifactsCount = () => findByTestId('artifacts-count');
+
+ const mountComponent = ({ propsData }) => {
+ wrapper = shallowMount(DetailsHeader, {
+ propsData,
+ stubs: {
+ TitleArea,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('artifact name', () => {
+ describe('missing image name', () => {
+ beforeEach(() => {
+ mountComponent({ propsData: { imagesDetail: { name: '', artifactCount: 1 } } });
+ });
+
+ it('root image', () => {
+ expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
+ });
+ });
+
+ describe('with artifact name present', () => {
+ beforeEach(() => {
+ mountComponent({ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } } });
+ });
+
+ it('shows artifact.name', () => {
+ expect(findTitle().text()).toContain('shao/flinkx');
+ });
+ });
+ });
+
+ describe('metadata items', () => {
+ describe('artifacts count', () => {
+ it('displays "-- artifacts" while loading', async () => {
+ mountComponent({ propsData: { imagesDetail: {} } });
+ await nextTick();
+
+ expect(findArtifactsCount().props('text')).toBe('-- artifacts');
+ });
+
+ it('when there is more than one artifact has the correct text', async () => {
+ mountComponent({ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 10 } } });
+
+ await nextTick();
+
+ expect(findArtifactsCount().props('text')).toBe('10 artifacts');
+ });
+
+ it('when there is one artifact has the correct text', async () => {
+ mountComponent({
+ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } },
+ });
+ await nextTick();
+
+ expect(findArtifactsCount().props('text')).toBe('1 artifact');
+ });
+
+ it('has the correct icon', async () => {
+ mountComponent({
+ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } },
+ });
+ await nextTick();
+
+ expect(findArtifactsCount().props('icon')).toBe('package');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
index 636f3eeb04a..7a6169d300c 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
@@ -7,14 +7,15 @@ import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import {
HARBOR_REGISTRY_TITLE,
LIST_INTRO_TEXT,
+ HARBOR_REGISTRY_HELP_PAGE_PATH,
} from '~/packages_and_registries/harbor_registry/constants/index';
describe('harbor_list_header', () => {
let wrapper;
- const findTitleArea = () => wrapper.find(TitleArea);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
- const findImagesMetaDataItem = () => wrapper.find(MetadataItem);
+ const findImagesMetaDataItem = () => wrapper.findComponent(MetadataItem);
const mountComponent = async (propsData, slots) => {
wrapper = shallowMount(HarborListHeader, {
@@ -77,10 +78,10 @@ describe('harbor_list_header', () => {
describe('info messages', () => {
describe('default message', () => {
it('is correctly bound to title_area props', () => {
- mountComponent({ helpPagePath: 'foo' });
+ mountComponent();
expect(findTitleArea().props('infoMessages')).toEqual([
- { text: LIST_INTRO_TEXT, link: 'foo' },
+ { text: LIST_INTRO_TEXT, link: HARBOR_REGISTRY_HELP_PAGE_PATH },
]);
});
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
index 8560c4f78f7..b62d4e8836b 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
@@ -1,25 +1,24 @@
import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils';
-import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { harborListResponse } from '../../mock_data';
+import { harborImagesList } from '../../mock_data';
describe('Harbor List Row', () => {
let wrapper;
- const [item] = harborListResponse.repositories;
+ const item = harborImagesList[0];
- const findDetailsLink = () => wrapper.find(RouterLink);
+ const findDetailsLink = () => wrapper.findComponent(RouterLink);
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
- const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
+ const findArtifactsCount = () => wrapper.find('[data-testid="artifacts-count"]');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const mountComponent = (props) => {
wrapper = shallowMount(HarborListRow, {
stubs: {
RouterLink,
- GlSprintf,
ListItem,
},
propsData: {
@@ -42,7 +41,8 @@ describe('Harbor List Row', () => {
expect(findDetailsLink().props('to')).toMatchObject({
name: 'details',
params: {
- id: item.id,
+ image: 'nginx',
+ project: 'nginx',
},
});
});
@@ -56,17 +56,17 @@ describe('Harbor List Row', () => {
});
});
- describe('tags count', () => {
+ describe('artifacts count', () => {
it('exists', () => {
mountComponent();
- expect(findTagsCount().exists()).toBe(true);
+ expect(findArtifactsCount().exists()).toBe(true);
});
- it('contains a tag icon', () => {
+ it('contains a package icon', () => {
mountComponent();
- const icon = findTagsCount().find(GlIcon);
+ const icon = findArtifactsCount().findComponent(GlIcon);
expect(icon.exists()).toBe(true);
- expect(icon.props('name')).toBe('tag');
+ expect(icon.props('name')).toBe('package');
});
describe('loading state', () => {
@@ -76,23 +76,23 @@ describe('Harbor List Row', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
- it('hides the tags count while loading', () => {
+ it('hides the artifacts count while loading', () => {
mountComponent({ metadataLoading: true });
- expect(findTagsCount().exists()).toBe(false);
+ expect(findArtifactsCount().exists()).toBe(false);
});
});
- describe('tags count text', () => {
- it('with one tag in the image', () => {
+ describe('artifacts count text', () => {
+ it('with one artifact in the image', () => {
mountComponent({ item: { ...item, artifactCount: 1 } });
- expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
+ expect(findArtifactsCount().text()).toMatchInterpolatedText('1 artifact');
});
- it('with more than one tag in the image', () => {
+ it('with more than one artifact in the image', () => {
mountComponent({ item: { ...item, artifactCount: 3 } });
- expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
+ expect(findArtifactsCount().text()).toMatchInterpolatedText('3 artifacts');
});
});
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
index f018eff58c9..e7e74a0da58 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
@@ -2,19 +2,19 @@ import { shallowMount } from '@vue/test-utils';
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
-import { harborListResponse } from '../../mock_data';
+import { harborImagesList } from '../../mock_data';
describe('Harbor List', () => {
let wrapper;
- const findHarborListRow = () => wrapper.findAll(HarborListRow);
+ const findHarborListRow = () => wrapper.findAllComponents(HarborListRow);
const mountComponent = (props) => {
wrapper = shallowMount(HarborList, {
stubs: { RegistryList },
propsData: {
- images: harborListResponse.repositories,
- pageInfo: harborListResponse.pageInfo,
+ images: harborImagesList,
+ pageInfo: {},
...props,
},
});
@@ -28,7 +28,7 @@ describe('Harbor List', () => {
it('contains one list element for each image', () => {
mountComponent();
- expect(findHarborListRow().length).toBe(harborListResponse.repositories.length);
+ expect(findHarborListRow().length).toBe(harborImagesList.length);
});
it('passes down the metadataLoading prop', () => {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js
new file mode 100644
index 00000000000..5e299a269e3
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js
@@ -0,0 +1,52 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { mockArtifactDetail, MOCK_SHA_DIGEST } from '../../mock_data';
+
+describe('Harbor Tags Header', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.findByTestId('title');
+ const findTagsCount = () => wrapper.findByTestId('tags-count');
+
+ const mountComponent = ({ propsData }) => {
+ wrapper = shallowMountExtended(TagsHeader, {
+ propsData,
+ stubs: {
+ TitleArea,
+ },
+ });
+ };
+
+ const mockPageInfo = {
+ page: 1,
+ perPage: 20,
+ total: 1,
+ totalPages: 1,
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ mountComponent({
+ propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false },
+ });
+ });
+
+ describe('tags title', () => {
+ it('should be artifact digest', () => {
+ expect(findTitle().text()).toBe(`sha256:${MOCK_SHA_DIGEST}`);
+ });
+ });
+
+ describe('tags count', () => {
+ it('would has the correct text', async () => {
+ await nextTick();
+
+ expect(findTagsCount().props('text')).toBe('1 tag');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
new file mode 100644
index 00000000000..6fe3dabc603
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
@@ -0,0 +1,75 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue';
+import { defaultConfig, harborTagsList } from '../../mock_data';
+
+describe('Harbor tag list row', () => {
+ let wrapper;
+
+ const findListItem = () => wrapper.find(ListItem);
+ const findClipboardButton = () => wrapper.find(ClipboardButton);
+ const findByTestId = (testId) => wrapper.findByTestId(testId);
+
+ const $route = {
+ params: {
+ project: defaultConfig.harborIntegrationProjectName,
+ image: 'test-repository',
+ },
+ };
+
+ const mountComponent = ({ propsData, config = defaultConfig }) => {
+ wrapper = shallowMountExtended(TagsListRow, {
+ stubs: {
+ ListItem,
+ GlSprintf,
+ },
+ propsData,
+ mocks: {
+ $route,
+ },
+ provide() {
+ return {
+ ...config,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('list item', () => {
+ beforeEach(() => {
+ mountComponent({
+ propsData: {
+ tag: harborTagsList[0],
+ },
+ });
+ });
+
+ it('exists', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ it('has the correct tag name', () => {
+ expect(findByTestId('name').text()).toBe(harborTagsList[0].name);
+ });
+
+ describe(' clipboard button', () => {
+ it('exists', () => {
+ expect(findClipboardButton().exists()).toBe(true);
+ });
+
+ it('has the correct props', () => {
+ const pullCommand = `docker pull demo.harbor.com/test-project/test-repository:${harborTagsList[0].name}`;
+ expect(findClipboardButton().attributes()).toMatchObject({
+ text: pullCommand,
+ title: pullCommand,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
new file mode 100644
index 00000000000..6bcf6611d07
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import { defaultConfig, harborTagsResponse } from '../../mock_data';
+
+describe('Harbor Tags List', () => {
+ let wrapper;
+
+ const findTagsLoader = () => wrapper.find(TagsLoader);
+ const findTagsListRows = () => wrapper.findAllComponents(TagsListRow);
+ const findRegistryList = () => wrapper.find(RegistryList);
+
+ const mountComponent = ({ propsData, config = defaultConfig }) => {
+ wrapper = shallowMount(TagsList, {
+ propsData,
+ stubs: { RegistryList },
+ provide() {
+ return {
+ ...config,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when isLoading is true', () => {
+ beforeEach(() => {
+ mountComponent({
+ propsData: {
+ isLoading: true,
+ pageInfo: {},
+ tags: [],
+ },
+ });
+ });
+
+ it('show the loader', () => {
+ expect(findTagsLoader().exists()).toBe(true);
+ });
+ });
+
+ describe('tags list', () => {
+ beforeEach(() => {
+ mountComponent({
+ propsData: {
+ isLoading: false,
+ pageInfo: {},
+ tags: harborTagsResponse,
+ },
+ });
+ });
+
+ it('should render correctly', () => {
+ expect(findRegistryList().exists()).toBe(true);
+ });
+
+ it('one tag row exists', () => {
+ expect(findTagsListRows()).toHaveLength(harborTagsResponse.length);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js
index 85399c22e79..b8989b6092e 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js
@@ -1,175 +1,114 @@
-export const harborListResponse = {
- repositories: [
- {
- artifactCount: 1,
- creationTime: '2022-03-02T06:35:53.205Z',
- id: 25,
- name: 'shao/flinkx',
- projectId: 21,
- pullCount: 0,
- updateTime: '2022-03-02T06:35:53.205Z',
- location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
- },
- {
- artifactCount: 1,
- creationTime: '2022-03-02T06:35:53.205Z',
- id: 26,
- name: 'shao/flinkx1',
- projectId: 21,
- pullCount: 0,
- updateTime: '2022-03-02T06:35:53.205Z',
- location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
- },
- {
- artifactCount: 1,
- creationTime: '2022-03-02T06:35:53.205Z',
- id: 27,
- name: 'shao/flinkx2',
- projectId: 21,
- pullCount: 0,
- updateTime: '2022-03-02T06:35:53.205Z',
- location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
- },
- ],
- totalCount: 3,
- pageInfo: {
- hasNextPage: false,
- hasPreviousPage: false,
- },
+export const harborImageDetailEmptyResponse = {
+ data: null,
};
-export const harborTagsResponse = {
- tags: [
- {
- digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
- name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
- revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255',
- shortRevision: 'f53bde3d4',
- createdAt: '2022-03-02T23:59:05+00:00',
- totalSize: '6623124',
- },
- {
- digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
- name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
- revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e',
- shortRevision: 'e1fe52d8b',
- createdAt: '2022-02-10T01:09:56+00:00',
- totalSize: '920760',
- },
- {
- digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
- name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
- revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f',
- shortRevision: 'c72770c6e',
- createdAt: '2021-12-22T04:48:48+00:00',
- totalSize: '48609053',
- },
- {
- digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
- name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
- revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a',
- shortRevision: '1ac2a4319',
- createdAt: '2022-03-09T11:02:27+00:00',
- totalSize: '35141894',
- },
- {
- digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
- name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
- revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c',
- shortRevision: 'cf8fee086',
- createdAt: '2022-01-21T11:31:43+00:00',
- totalSize: '48716070',
- },
- {
- digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
- name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
- revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15',
- shortRevision: '1a4b48198',
- createdAt: '2022-01-21T11:31:51+00:00',
- totalSize: '6623127',
- },
- {
- digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
- name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
- revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61',
- shortRevision: '03e2e2777',
- createdAt: '2022-03-02T23:58:20+00:00',
- totalSize: '911377',
- },
- {
- digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
- name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
- revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012',
- shortRevision: '350e78d60',
- createdAt: '2022-01-19T13:49:14+00:00',
- totalSize: '48710241',
- },
- {
- digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
- name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
- revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18',
- shortRevision: '76038370b',
- createdAt: '2022-01-24T12:56:22+00:00',
- totalSize: '280065',
- },
- {
- digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07',
- location:
- 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
- path:
- 'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
- name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
- revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f',
- shortRevision: '3d4b49a7b',
- createdAt: '2022-02-17T17:37:52+00:00',
- totalSize: '48655767',
- },
- ],
- totalCount: 100,
- pageInfo: {
- hasNextPage: false,
- hasPreviousPage: false,
+export const MOCK_SHA_DIGEST = 'mock_sha_digest_value';
+
+export const harborImageDetailResponse = {
+ artifactCount: 10,
+ creationTime: '2022-03-02T06:35:53.205Z',
+ id: 25,
+ name: 'shao/flinkx',
+ projectId: 21,
+ pullCount: 0,
+ updateTime: '2022-03-02T06:35:53.205Z',
+ location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
+};
+
+export const harborArtifactsResponse = [
+ {
+ id: 1,
+ digest: `sha256:${MOCK_SHA_DIGEST}`,
+ size: 773928,
+ push_time: '2022-05-19T15:54:47.821Z',
+ tags: ['latest'],
+ },
+];
+
+export const harborArtifactsList = [
+ {
+ id: 1,
+ digest: `sha256:${MOCK_SHA_DIGEST}`,
+ size: 773928,
+ pushTime: '2022-05-19T15:54:47.821Z',
+ tags: ['latest'],
+ },
+];
+
+export const harborTagsResponse = [
+ {
+ repository_id: 4,
+ artifact_id: 5,
+ id: 4,
+ name: 'latest',
+ pull_time: '0001-01-01T00:00:00.000Z',
+ push_time: '2022-05-27T18:21:27.903Z',
+ signed: false,
+ immutable: false,
+ },
+];
+
+export const harborTagsList = [
+ {
+ repositoryId: 4,
+ artifactId: 5,
+ id: 4,
+ name: 'latest',
+ pullTime: '0001-01-01T00:00:00.000Z',
+ pushTime: '2022-05-27T18:21:27.903Z',
+ signed: false,
+ immutable: false,
},
+];
+
+export const defaultConfig = {
+ noContainersImage: 'noContainersImage',
+ repositoryUrl: 'demo.harbor.com',
+ harborIntegrationProjectName: 'test-project',
+ projectName: 'Flight',
+ endpoint: '/flightjs/Flight/-/harbor/repositories',
+ connectionError: false,
+ invalidPathError: false,
+ isGroupPage: false,
+ containersErrorImage: 'containersErrorImage',
};
+export const defaultFullPath = 'flightjs/Flight';
+
+export const harborImagesResponse = [
+ {
+ id: 1,
+ name: 'nginx/nginx',
+ artifact_count: 1,
+ creation_time: '2022-05-29T10:07:16.812Z',
+ update_time: '2022-05-29T10:07:16.812Z',
+ project_id: 4,
+ pull_count: 0,
+ location: 'https://demo.goharbor.io/harbor/projects/4/repositories/nginx',
+ },
+];
+
+export const harborImagesList = [
+ {
+ id: 1,
+ name: 'nginx/nginx',
+ artifactCount: 1,
+ creationTime: '2022-05-29T10:07:16.812Z',
+ updateTime: '2022-05-29T10:07:16.812Z',
+ projectId: 4,
+ pullCount: 0,
+ location: 'https://demo.goharbor.io/harbor/projects/4/repositories/nginx',
+ },
+];
+
export const dockerCommands = {
dockerBuildCommand: 'foofoo',
dockerPushCommand: 'barbar',
dockerLoginCommand: 'bazbaz',
};
+
+export const mockArtifactDetail = {
+ project: 'test-project',
+ image: 'test-repository',
+ digest: `sha256:${MOCK_SHA_DIGEST}`,
+};
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
new file mode 100644
index 00000000000..8fd50bea280
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
@@ -0,0 +1,162 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import HarborDetailsPage from '~/packages_and_registries/harbor_registry/pages/details.vue';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ NAME_SORT_FIELD,
+ TOKEN_TYPE_TAG_NAME,
+} from '~/packages_and_registries/harbor_registry/constants/index';
+import { harborArtifactsResponse, harborArtifactsList, defaultConfig } from '../mock_data';
+
+let mockHarborArtifactsResponse;
+
+jest.mock('~/rest_api', () => ({
+ getHarborArtifacts: () => mockHarborArtifactsResponse,
+}));
+
+describe('Harbor Details Page', () => {
+ let wrapper;
+
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const findArtifactsList = () => wrapper.findComponent(ArtifactsList);
+ const findDetailsHeader = () => wrapper.findComponent(DetailsHeader);
+ const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
+
+ const waitForHarborDetailRequest = async () => {
+ await waitForPromises();
+ await nextTick();
+ };
+
+ const $route = {
+ params: {
+ project: 'test-project',
+ image: 'test-repository',
+ },
+ };
+
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ updateHref: jest.fn(),
+ };
+
+ const defaultHeaders = {
+ 'x-page': '1',
+ 'X-Per-Page': '20',
+ 'X-TOTAL': '1',
+ 'X-Total-Pages': '1',
+ };
+
+ const mountComponent = ({ config = defaultConfig } = {}) => {
+ wrapper = shallowMount(HarborDetailsPage, {
+ mocks: {
+ $route,
+ },
+ provide() {
+ return {
+ breadCrumbState,
+ ...config,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockHarborArtifactsResponse = Promise.resolve({
+ data: harborArtifactsResponse,
+ headers: defaultHeaders,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when isLoading is true', () => {
+ it('shows the loader', () => {
+ mountComponent();
+
+ expect(findTagsLoader().exists()).toBe(true);
+ });
+
+ it('does not show the list', () => {
+ mountComponent();
+
+ expect(findArtifactsList().exists()).toBe(false);
+ });
+ });
+
+ describe('artifacts list', () => {
+ it('exists', async () => {
+ mountComponent();
+
+ findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
+ await waitForHarborDetailRequest();
+
+ expect(findArtifactsList().exists()).toBe(true);
+ });
+
+ it('has the correct props bound', async () => {
+ mountComponent();
+
+ findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
+ await waitForHarborDetailRequest();
+
+ expect(findArtifactsList().props()).toMatchObject({
+ isLoading: false,
+ filter: '',
+ artifacts: harborArtifactsList,
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ total: 1,
+ totalPages: 1,
+ },
+ });
+ });
+ });
+
+ describe('persisted search', () => {
+ it('has the correct props', () => {
+ mountComponent();
+
+ expect(findPersistedSearch().props()).toMatchObject({
+ sortableFields: [NAME_SORT_FIELD],
+ defaultOrder: NAME_SORT_FIELD.orderBy,
+ defaultSort: 'asc',
+ tokens: [
+ {
+ type: TOKEN_TYPE_TAG_NAME,
+ icon: 'tag',
+ title: s__('HarborRegistry|Tag'),
+ unique: true,
+ token: GlFilteredSearchToken,
+ operators: OPERATOR_IS_ONLY,
+ },
+ ],
+ });
+ });
+ });
+
+ describe('header', () => {
+ it('has the correct props', async () => {
+ mountComponent();
+
+ findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
+ await waitForHarborDetailRequest();
+
+ expect(findDetailsHeader().props()).toMatchObject({
+ imagesDetail: {
+ name: 'test-project/test-repository',
+ artifactCount: 1,
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
index 55fc8066f65..942cf9bad2c 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
@@ -4,7 +4,7 @@ import component from '~/packages_and_registries/harbor_registry/pages/index.vue
describe('List Page', () => {
let wrapper;
- const findRouterView = () => wrapper.find({ ref: 'router-view' });
+ const findRouterView = () => wrapper.findComponent({ ref: 'router-view' });
const mountComponent = () => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
index 61ee36a2794..97d30e6fe99 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
@@ -5,15 +5,14 @@ import HarborListHeader from '~/packages_and_registries/harbor_registry/componen
import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import waitForPromises from 'helpers/wait_for_promises';
-// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js';
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index';
-import { harborListResponse, dockerCommands } from '../mock_data';
+import { harborImagesResponse, defaultConfig, harborImagesList } from '../mock_data';
let mockHarborListResponse;
-jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({
- harborListResponse: () => mockHarborListResponse,
+jest.mock('~/rest_api', () => ({
+ getHarborRepositoriesList: () => mockHarborListResponse,
}));
describe('Harbor List Page', () => {
@@ -24,34 +23,43 @@ describe('Harbor List Page', () => {
await nextTick();
};
- beforeEach(() => {
- mockHarborListResponse = Promise.resolve(harborListResponse);
- });
-
const findHarborListHeader = () => wrapper.findComponent(HarborListHeader);
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findHarborList = () => wrapper.findComponent(HarborList);
const findCliCommands = () => wrapper.findComponent(CliCommands);
+ const defaultHeaders = {
+ 'x-page': '1',
+ 'X-Per-Page': '20',
+ 'X-TOTAL': '1',
+ 'X-Total-Pages': '1',
+ };
+
const fireFirstSortUpdate = () => {
findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
};
- const mountComponent = ({ config = { isGroupPage: false } } = {}) => {
+ const mountComponent = ({ config = defaultConfig } = {}) => {
wrapper = shallowMount(HarborRegistryList, {
stubs: {
HarborListHeader,
},
provide() {
return {
- config,
- ...dockerCommands,
+ ...config,
};
},
});
};
+ beforeEach(() => {
+ mockHarborListResponse = Promise.resolve({
+ data: harborImagesResponse,
+ headers: defaultHeaders,
+ });
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -64,7 +72,7 @@ describe('Harbor List Page', () => {
expect(findHarborListHeader().exists()).toBe(true);
expect(findHarborListHeader().props()).toMatchObject({
- imagesCount: 3,
+ imagesCount: 1,
metadataLoading: false,
});
});
@@ -117,6 +125,16 @@ describe('Harbor List Page', () => {
await nextTick();
expect(findHarborList().exists()).toBe(true);
+ expect(findHarborList().props()).toMatchObject({
+ images: harborImagesList,
+ metadataLoading: false,
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ total: 1,
+ totalPages: 1,
+ },
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
new file mode 100644
index 00000000000..7e0f05e736b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
@@ -0,0 +1,125 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import HarborTagsPage from '~/packages_and_registries/harbor_registry/pages/harbor_tags.vue';
+import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue';
+import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { defaultConfig, harborTagsResponse, mockArtifactDetail } from '../mock_data';
+
+let mockHarborTagsResponse;
+
+jest.mock('~/rest_api', () => ({
+ getHarborTags: () => mockHarborTagsResponse,
+}));
+
+describe('Harbor Tags page', () => {
+ let wrapper;
+
+ const findTagsHeader = () => wrapper.find(TagsHeader);
+ const findTagsList = () => wrapper.find(TagsList);
+
+ const waitForHarborTagsRequest = async () => {
+ await waitForPromises();
+ await nextTick();
+ };
+
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ updateHref: jest.fn(),
+ };
+
+ const $route = {
+ params: mockArtifactDetail,
+ };
+
+ const defaultHeaders = {
+ 'x-page': '1',
+ 'X-Per-Page': '20',
+ 'X-TOTAL': '1',
+ 'X-Total-Pages': '1',
+ };
+
+ const mountComponent = ({ endpoint = defaultConfig.endpoint } = {}) => {
+ wrapper = shallowMount(HarborTagsPage, {
+ mocks: {
+ $route,
+ },
+ provide() {
+ return {
+ breadCrumbState,
+ endpoint,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockHarborTagsResponse = Promise.resolve({
+ data: harborTagsResponse,
+ headers: defaultHeaders,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains tags header', () => {
+ mountComponent();
+
+ expect(findTagsHeader().exists()).toBe(true);
+ });
+
+ it('contains tags list', () => {
+ mountComponent();
+
+ expect(findTagsList().exists()).toBe(true);
+ });
+
+ describe('header', () => {
+ it('has the correct props', async () => {
+ mountComponent();
+
+ await waitForHarborTagsRequest();
+ expect(findTagsHeader().props()).toMatchObject({
+ artifactDetail: mockArtifactDetail,
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ total: 1,
+ totalPages: 1,
+ },
+ tagsLoading: false,
+ });
+ });
+ });
+
+ describe('list', () => {
+ it('has the correct props', async () => {
+ mountComponent();
+
+ await waitForHarborTagsRequest();
+ expect(findTagsList().props()).toMatchObject({
+ tags: [
+ {
+ repositoryId: 4,
+ artifactId: 5,
+ id: 4,
+ name: 'latest',
+ pullTime: '0001-01-01T00:00:00.000Z',
+ pushTime: '2022-05-27T18:21:27.903Z',
+ signed: false,
+ immutable: false,
+ },
+ ],
+ isLoading: false,
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ total: 1,
+ totalPages: 1,
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
index 69c78e64e22..e74375b7705 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
@@ -76,8 +76,8 @@ describe('PackagesApp', () => {
const packageTitle = () => wrapper.findComponent(TerraformTitle);
const emptyState = () => wrapper.findComponent(GlEmptyState);
const deleteButton = () => wrapper.find('.js-delete-button');
- const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' });
- const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' });
+ const findDeleteModal = () => wrapper.findComponent({ ref: 'deleteModal' });
+ const findDeleteFileModal = () => wrapper.findComponent({ ref: 'deleteFileModal' });
const versionsTab = () => wrapper.find('.js-versions-tab > a');
const packagesLoader = () => wrapper.findComponent(PackagesListLoader);
const packagesVersionRows = () => wrapper.findAllComponents(PackageListRow);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
index 95de2f0bb0b..b76d7c2b57b 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
@@ -17,8 +17,8 @@ describe('Package Files', () => {
const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]');
const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]');
const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]');
- const findFirstRowFileIcon = () => findFirstRow().find(FileIcon);
- const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip);
+ const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon);
+ const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip);
const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown);
const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]');
const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
index f10f05f4a0d..c6b5138639e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
@@ -36,8 +36,8 @@ describe('Package History', () => {
});
const findHistoryElement = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findElementLink = (container) => container.find(GlLink);
- const findElementTimeAgo = (container) => container.find(TimeAgoTooltip);
+ const findElementLink = (container) => container.findComponent(GlLink);
+ const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip);
const findTitle = () => wrapper.find('[data-testid="title"]');
const findTimeline = () => wrapper.find('[data-testid="timeline"]');
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index 72d08d5683b..93d013bb458 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -7,8 +7,8 @@ describe('Infrastructure Title', () => {
let wrapper;
let store;
- const findTitleArea = () => wrapper.find(TitleArea);
- const findMetadataItem = () => wrapper.find(MetadataItem);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
+ const findMetadataItem = () => wrapper.findComponent(MetadataItem);
const exampleProps = { helpUrl: 'http://example.gitlab.com/help' };
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index 31616e0b2f5..db1d3f3f633 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -31,9 +31,9 @@ describe('packages_list_app', () => {
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const emptyListHelpUrl = 'helpUrl';
- const findEmptyState = () => wrapper.find(GlEmptyState);
- const findListComponent = () => wrapper.find(PackageList);
- const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findListComponent = () => wrapper.findComponent(PackageList);
+ const findInfrastructureSearch = () => wrapper.findComponent(InfrastructureSearch);
const createStore = ({ filter = [], packageCount = 0 } = {}) => {
store = new Vuex.Store({
@@ -151,7 +151,7 @@ describe('packages_list_app', () => {
describe('empty state', () => {
it('generate the correct empty list link', () => {
- const link = findListComponent().find(GlLink);
+ const link = findListComponent().findComponent(GlLink);
expect(link.attributes('href')).toBe(emptyListHelpUrl);
expect(link.text()).toBe('publish and share your packages');
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index fed82653016..fb5ee4e6884 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -20,11 +20,11 @@ describe('packages_list', () => {
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
- const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
- const findPackageListPagination = () => wrapper.find(GlPagination);
- const findPackageListDeleteModal = () => wrapper.find(GlModal);
- const findEmptySlot = () => wrapper.find(EmptySlotStub);
- const findPackagesListRow = () => wrapper.find(PackagesListRow);
+ const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
+ const findPackageListPagination = () => wrapper.findComponent(GlPagination);
+ const findPackageListDeleteModal = () => wrapper.findComponent(GlModal);
+ const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
+ const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const createStore = (isGroupPage, packages, isLoading) => {
const state = {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
index 67c3b8b795a..91824dee5b0 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
@@ -3,7 +3,7 @@
exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
- data-qa-selector="package_row"
+ data-testid="package-row"
>
<div
class="gl-display-flex gl-align-items-center gl-py-3"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js
index abb0d23b6e4..db90bb4c25f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js
@@ -5,7 +5,7 @@ import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_
describe('InfrastructureIconAndName', () => {
let wrapper;
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const mountComponent = () => {
wrapper = shallowMount(InfrastructureIconAndName, {});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
index d324d43258c..9449c40c7c6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
@@ -71,7 +71,7 @@ describe('NugetInstallation', () => {
});
});
- it('it has docs link', () => {
+ it('has docs link', () => {
expect(findSetupDocsLink().attributes()).toMatchObject({
href: NUGET_HELP_PATH,
target: '_blank',
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 031afa62890..5be05ddf629 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -3,7 +3,7 @@
exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
- data-qa-selector="package_row"
+ data-testid="package-row"
>
<div
class="gl-display-flex gl-align-items-center gl-py-3"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index eb1e76377ff..b5a512b8806 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -30,10 +30,10 @@ describe('packages_list_row', () => {
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
const packageCannotDestroy = { ...packageData(), canDestroy: false };
- const findPackageTags = () => wrapper.find(PackageTags);
- const findPackagePath = () => wrapper.find(PackagePath);
+ const findPackageTags = () => wrapper.findComponent(PackageTags);
+ const findPackagePath = () => wrapper.findComponent(PackagePath);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
- const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
+ const findPackageIconAndName = () => wrapper.findComponent(PackageIconAndName);
const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 660f00a2b31..3e3607a361c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -190,7 +190,7 @@ describe('packages_list', () => {
});
});
- describe('pagination ', () => {
+ describe('pagination', () => {
beforeEach(() => {
mountComponent({ pageInfo: { hasPreviousPage: true } });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
index 23e5c7330d5..b47515e15c3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -7,8 +7,8 @@ describe('PackageTitle', () => {
let wrapper;
let store;
- const findTitleArea = () => wrapper.find(TitleArea);
- const findMetadataItem = () => wrapper.find(MetadataItem);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
+ const findMetadataItem = () => wrapper.findComponent(MetadataItem);
const mountComponent = (propsData = { helpUrl: 'foo' }) => {
wrapper = shallowMount(PackageTitle, {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
index d0c111bae2d..8f3c8667c47 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -6,8 +6,8 @@ import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/consta
describe('packages_filter', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findFilteredSearchSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion);
const mountComponent = ({ attrs, listeners } = {}) => {
wrapper = shallowMount(component, {
@@ -24,13 +24,13 @@ describe('packages_filter', () => {
wrapper = null;
});
- it('it binds all of his attrs to filtered search token', () => {
+ it('binds all of his attrs to filtered search token', () => {
mountComponent({ attrs: { foo: 'bar' } });
expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
});
- it('it binds all of his events to filtered search token', () => {
+ it('binds all of his events to filtered search token', () => {
const clickListener = jest.fn();
mountComponent({ listeners: { click: clickListener } });
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index de78e6bb87b..83158d1cc5e 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -178,7 +178,7 @@ describe('PackagesApp', () => {
${PACKAGE_TYPE_PYPI} | ${true}
${PACKAGE_TYPE_NPM} | ${false}
`(
- `It is $visible that the component is visible when the package is $packageType`,
+ `is $visible that the component is visible when the package is $packageType`,
async ({ packageType, visible }) => {
createComponent({
resolver: jest.fn().mockResolvedValue(
@@ -328,8 +328,8 @@ describe('PackagesApp', () => {
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
- expect(showDeletePackageSpy).not.toBeCalled();
- expect(showDeleteFileSpy).toBeCalled();
+ expect(showDeletePackageSpy).not.toHaveBeenCalled();
+ expect(showDeleteFileSpy).toHaveBeenCalled();
});
it('when its the only file opens delete package confirmation modal', async () => {
@@ -357,8 +357,8 @@ describe('PackagesApp', () => {
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
- expect(showDeletePackageSpy).toBeCalled();
- expect(showDeleteFileSpy).not.toBeCalled();
+ expect(showDeletePackageSpy).toHaveBeenCalled();
+ expect(showDeleteFileSpy).not.toHaveBeenCalled();
});
it('confirming on the modal sets the loading state', async () => {
@@ -443,7 +443,7 @@ describe('PackagesApp', () => {
findPackageFiles().vm.$emit('delete-files', packageFiles());
- expect(showDeleteFilesSpy).toBeCalled();
+ expect(showDeleteFilesSpy).toHaveBeenCalled();
});
it('confirming on the modal sets the loading state', async () => {
@@ -532,7 +532,7 @@ describe('PackagesApp', () => {
findPackageFiles().vm.$emit('delete-files', packageFiles());
- expect(showDeletePackageSpy).toBeCalled();
+ expect(showDeletePackageSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap b/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap
deleted file mode 100644
index 5b56cb7f74e..00000000000
--- a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`settings_titles renders properly 1`] = `
-<div>
- <h5
- class="gl-border-b-solid gl-border-b-1 gl-border-gray-200 gl-pb-3"
- >
-
- foo
-
- </h5>
-
- <p>
- bar
- </p>
-
-</div>
-`;
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
index 9d4c7f4737b..796d89231f4 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -169,7 +169,7 @@ describe('DependencyProxySettings', () => {
toggleName | toggleFinder | localErrorMock | optimisticResponse
${'enable proxy'} | ${findEnableProxyToggle} | ${dependencyProxySettingMutationMock} | ${updateGroupDependencyProxySettingsOptimisticResponse}
${'enable ttl policies'} | ${findEnableTtlPoliciesToggle} | ${dependencyProxyUpdateTllPolicyMutationMock} | ${updateDependencyProxyImageTtlGroupPolicyOptimisticResponse}
- `('$toggleName settings update ', ({ optimisticResponse, toggleFinder, localErrorMock }) => {
+ `('$toggleName settings update', ({ optimisticResponse, toggleFinder, localErrorMock }) => {
describe('success state', () => {
it('emits a success event', async () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
deleted file mode 100644
index 3eecdeb5b1f..00000000000
--- a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import component from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
-
-import {
- DUPLICATES_TOGGLE_LABEL,
- DUPLICATES_SETTING_EXCEPTION_TITLE,
- DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
-} from '~/packages_and_registries/settings/group/constants';
-
-describe('Duplicates Settings', () => {
- let wrapper;
-
- const defaultProps = {
- duplicatesAllowed: false,
- duplicateExceptionRegex: 'foo',
- modelNames: {
- allowed: 'allowedModel',
- exception: 'exceptionModel',
- },
- };
-
- const mountComponent = (propsData = defaultProps) => {
- wrapper = shallowMount(component, {
- propsData,
- stubs: {
- GlSprintf,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findToggle = () => wrapper.findComponent(GlToggle);
-
- const findInputGroup = () => wrapper.findComponent(GlFormGroup);
- const findInput = () => wrapper.findComponent(GlFormInput);
-
- it('has a toggle', () => {
- mountComponent();
-
- expect(findToggle().exists()).toBe(true);
- expect(findToggle().props()).toMatchObject({
- label: DUPLICATES_TOGGLE_LABEL,
- value: !defaultProps.duplicatesAllowed,
- });
- });
-
- it('toggle emits an update event', () => {
- mountComponent();
-
- findToggle().vm.$emit('change', false);
-
- expect(wrapper.emitted('update')).toStrictEqual([
- [{ [defaultProps.modelNames.allowed]: true }],
- ]);
- });
-
- describe('when the duplicates are disabled', () => {
- it('shows a form group with an input field', () => {
- mountComponent();
-
- expect(findInputGroup().exists()).toBe(true);
-
- expect(findInputGroup().attributes()).toMatchObject({
- 'label-for': 'maven-duplicated-settings-regex-input',
- label: DUPLICATES_SETTING_EXCEPTION_TITLE,
- description: DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
- });
- });
-
- it('shows an input field', () => {
- mountComponent();
-
- expect(findInput().exists()).toBe(true);
-
- expect(findInput().attributes()).toMatchObject({
- id: 'maven-duplicated-settings-regex-input',
- value: defaultProps.duplicateExceptionRegex,
- });
- });
-
- it('input change event emits an update event', () => {
- mountComponent();
-
- findInput().vm.$emit('change', 'bar');
-
- expect(wrapper.emitted('update')).toStrictEqual([
- [{ [defaultProps.modelNames.exception]: 'bar' }],
- ]);
- });
-
- describe('valid state', () => {
- it('form group has correct props', () => {
- mountComponent();
-
- expect(findInputGroup().attributes()).toMatchObject({
- state: 'true',
- 'invalid-feedback': '',
- });
- });
- });
-
- describe('invalid state', () => {
- it('form group has correct props', () => {
- const propsWithError = {
- ...defaultProps,
- duplicateExceptionRegexError: 'some error string',
- };
-
- mountComponent(propsWithError);
-
- expect(findInputGroup().attributes()).toMatchObject({
- 'invalid-feedback': propsWithError.duplicateExceptionRegexError,
- });
- });
- });
- });
-
- describe('when the duplicates are enabled', () => {
- it('hides the form input group', () => {
- mountComponent({ ...defaultProps, duplicatesAllowed: true });
-
- expect(findInputGroup().exists()).toBe(false);
- });
- });
-
- describe('loading', () => {
- beforeEach(() => {
- mountComponent({ ...defaultProps, loading: true });
- });
-
- it('disables the enable toggle', () => {
- expect(findToggle().props('disabled')).toBe(true);
- });
-
- it('disables the form input', () => {
- expect(findInput().attributes('disabled')).toBe('true');
- });
- });
-});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js
new file mode 100644
index 00000000000..86f14961690
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js
@@ -0,0 +1,108 @@
+import { GlSprintf, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages_and_registries/settings/group/components/exceptions_input.vue';
+
+import { DUPLICATES_SETTING_EXCEPTION_TITLE } from '~/packages_and_registries/settings/group/constants';
+
+describe('Exceptions Input', () => {
+ let wrapper;
+
+ const defaultProps = {
+ duplicatesAllowed: false,
+ duplicateExceptionRegex: 'foo',
+ id: 'maven-duplicated-settings-regex-input',
+ name: 'exceptionModel',
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+
+ it('shows a form group with an input field', () => {
+ mountComponent();
+
+ expect(findInputGroup().exists()).toBe(true);
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ 'label-for': defaultProps.id,
+ label: DUPLICATES_SETTING_EXCEPTION_TITLE,
+ 'label-sr-only': '',
+ });
+ });
+
+ it('shows an input field', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+
+ expect(findInput().attributes()).toMatchObject({
+ id: 'maven-duplicated-settings-regex-input',
+ value: defaultProps.duplicateExceptionRegex,
+ });
+ });
+
+ it('input change event emits an update event', () => {
+ mountComponent();
+
+ findInput().vm.$emit('change', 'bar');
+
+ expect(wrapper.emitted('update')).toStrictEqual([[{ [defaultProps.name]: 'bar' }]]);
+ });
+
+ describe('valid state', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('form group has correct props', () => {
+ expect(findInputGroup().attributes('input-feedback')).toBeUndefined();
+ });
+
+ it('form input has correct props', () => {
+ expect(findInput().attributes('state')).toBe('true');
+ });
+ });
+
+ describe('invalid state', () => {
+ const propsWithError = {
+ ...defaultProps,
+ duplicateExceptionRegexError: 'some error string',
+ };
+
+ beforeEach(() => {
+ mountComponent(propsWithError);
+ });
+
+ it('form group has correct props', () => {
+ expect(findInputGroup().attributes('invalid-feedback')).toBe(
+ propsWithError.duplicateExceptionRegexError,
+ );
+ });
+
+ it('form input has correct props', () => {
+ expect(findInput().attributes('state')).toBeUndefined();
+ });
+ });
+
+ describe('loading', () => {
+ beforeEach(() => {
+ mountComponent({ ...defaultProps, loading: true });
+ });
+
+ it('disables the form input', () => {
+ expect(findInput().attributes('disabled')).toBe('true');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js
deleted file mode 100644
index 4eafeedd55e..00000000000
--- a/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
-import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
-
-describe('generic_settings', () => {
- let wrapper;
-
- const mountComponent = () => {
- wrapper = shallowMount(GenericSettings, {
- scopedSlots: {
- default: '<div data-testid="default-slot">{{props.modelNames}}</div>',
- },
- });
- };
-
- const findSettingsTitle = () => wrapper.findComponent(SettingsTitles);
- const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('title component', () => {
- it('has a title component', () => {
- mountComponent();
-
- expect(findSettingsTitle().exists()).toBe(true);
- });
-
- it('passes the correct props', () => {
- mountComponent();
-
- expect(findSettingsTitle().props()).toMatchObject({
- title: 'Generic',
- subTitle: 'Settings for Generic packages',
- });
- });
- });
-
- describe('default slot', () => {
- it('accept a default slots', () => {
- mountComponent();
-
- expect(findDefaultSlot().exists()).toBe(true);
- });
-
- it('binds model names', () => {
- mountComponent();
-
- expect(findDefaultSlot().text()).toContain('genericDuplicatesAllowed');
- expect(findDefaultSlot().text()).toContain('genericDuplicateExceptionRegex');
- });
- });
-});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
deleted file mode 100644
index 22644b97b43..00000000000
--- a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
-import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
-
-describe('maven_settings', () => {
- let wrapper;
-
- const mountComponent = () => {
- wrapper = shallowMount(MavenSettings, {
- scopedSlots: {
- default: '<div data-testid="default-slot">{{props.modelNames}}</div>',
- },
- });
- };
-
- const findSettingsTitle = () => wrapper.findComponent(SettingsTitles);
- const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('title component', () => {
- it('has a title component', () => {
- mountComponent();
-
- expect(findSettingsTitle().exists()).toBe(true);
- });
-
- it('passes the correct props', () => {
- mountComponent();
-
- expect(findSettingsTitle().props()).toMatchObject({
- title: 'Maven',
- subTitle: 'Settings for Maven packages',
- });
- });
- });
-
- describe('default slot', () => {
- it('accept a default slots', () => {
- mountComponent();
-
- expect(findDefaultSlot().exists()).toBe(true);
- });
-
- it('binds model names', () => {
- mountComponent();
-
- expect(findDefaultSlot().text()).toContain('mavenDuplicatesAllowed');
- expect(findDefaultSlot().text()).toContain('mavenDuplicateExceptionRegex');
- });
- });
-});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 274930ce668..13eba39ec8c 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -1,13 +1,13 @@
import Vue, { nextTick } from 'vue';
+import { GlToggle } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
-import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
+import ExceptionsInput from '~/packages_and_registries/settings/group/components/exceptions_input.vue';
import component from '~/packages_and_registries/settings/group/components/packages_settings.vue';
-import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
import {
+ DUPLICATES_TOGGLE_LABEL,
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
} from '~/packages_and_registries/settings/group/constants';
@@ -35,6 +35,7 @@ describe('Packages Settings', () => {
};
const mountComponent = ({
+ mountFn = shallowMountExtended,
mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
} = {}) => {
Vue.use(VueApollo);
@@ -43,7 +44,7 @@ describe('Packages Settings', () => {
apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMountExtended(component, {
+ wrapper = mountFn(component, {
apolloProvider,
provide: defaultProvide,
propsData: {
@@ -51,8 +52,6 @@ describe('Packages Settings', () => {
},
stubs: {
SettingsBlock,
- MavenSettings,
- GenericSettings,
},
});
};
@@ -63,11 +62,15 @@ describe('Packages Settings', () => {
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findDescription = () => wrapper.findByTestId('description');
- const findMavenSettings = () => wrapper.findComponent(MavenSettings);
- const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings);
- const findGenericSettings = () => wrapper.findComponent(GenericSettings);
- const findGenericDuplicatedSettings = () =>
- findGenericSettings().findComponent(DuplicatesSettings);
+ const findMavenSettings = () => wrapper.findByTestId('maven-settings');
+ const findGenericSettings = () => wrapper.findByTestId('generic-settings');
+
+ const findMavenDuplicatedSettingsToggle = () => findMavenSettings().findComponent(GlToggle);
+ const findGenericDuplicatedSettingsToggle = () => findGenericSettings().findComponent(GlToggle);
+ const findMavenDuplicatedSettingsExceptionsInput = () =>
+ findMavenSettings().findComponent(ExceptionsInput);
+ const findGenericDuplicatedSettingsExceptionsInput = () =>
+ findGenericSettings().findComponent(ExceptionsInput);
const fillApolloCache = () => {
apolloProvider.defaultClient.cache.writeQuery({
@@ -80,7 +83,7 @@ describe('Packages Settings', () => {
};
const emitMavenSettingsUpdate = (override) => {
- findMavenDuplicatedSettings().vm.$emit('update', {
+ findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', {
mavenDuplicateExceptionRegex: ')',
...override,
});
@@ -106,27 +109,46 @@ describe('Packages Settings', () => {
describe('maven settings', () => {
it('exists', () => {
- mountComponent();
+ mountComponent({ mountFn: mountExtended });
+
+ expect(findMavenSettings().find('td').text()).toBe('Maven');
+ });
+
+ it('renders toggle', () => {
+ mountComponent({ mountFn: mountExtended });
- expect(findMavenSettings().exists()).toBe(true);
+ const { mavenDuplicatesAllowed } = packageSettings();
+
+ expect(findMavenDuplicatedSettingsToggle().exists()).toBe(true);
+
+ expect(findMavenDuplicatedSettingsToggle().props()).toMatchObject({
+ label: DUPLICATES_TOGGLE_LABEL,
+ value: mavenDuplicatesAllowed,
+ disabled: false,
+ labelPosition: 'hidden',
+ });
});
- it('assigns duplication allowness and exception props', async () => {
- mountComponent();
+ it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
+ mountComponent({ mountFn: mountExtended });
const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings();
- expect(findMavenDuplicatedSettings().props()).toMatchObject({
+ expect(findMavenDuplicatedSettingsExceptionsInput().exists()).toBe(true);
+
+ expect(findMavenDuplicatedSettingsExceptionsInput().props()).toMatchObject({
duplicatesAllowed: mavenDuplicatesAllowed,
duplicateExceptionRegex: mavenDuplicateExceptionRegex,
duplicateExceptionRegexError: '',
loading: false,
+ name: 'mavenDuplicateExceptionRegex',
+ id: 'maven-duplicated-settings-regex-input',
});
});
it('on update event calls the mutation', () => {
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
- mountComponent({ mutationResolver });
+ mountComponent({ mountFn: mountExtended, mutationResolver });
fillApolloCache();
@@ -140,31 +162,47 @@ describe('Packages Settings', () => {
describe('generic settings', () => {
it('exists', () => {
- mountComponent();
+ mountComponent({ mountFn: mountExtended });
- expect(findGenericSettings().exists()).toBe(true);
+ expect(findGenericSettings().find('td').text()).toBe('Generic');
});
- it('assigns duplication allowness and exception props', async () => {
- mountComponent();
+ it('renders toggle', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ const { genericDuplicatesAllowed } = packageSettings();
+
+ expect(findGenericDuplicatedSettingsToggle().exists()).toBe(true);
+ expect(findGenericDuplicatedSettingsToggle().props()).toMatchObject({
+ label: DUPLICATES_TOGGLE_LABEL,
+ value: genericDuplicatesAllowed,
+ disabled: false,
+ labelPosition: 'hidden',
+ });
+ });
+
+ it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => {
+ mountComponent({ mountFn: mountExtended });
const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings();
- expect(findGenericDuplicatedSettings().props()).toMatchObject({
+ expect(findGenericDuplicatedSettingsExceptionsInput().props()).toMatchObject({
duplicatesAllowed: genericDuplicatesAllowed,
duplicateExceptionRegex: genericDuplicateExceptionRegex,
duplicateExceptionRegexError: '',
loading: false,
+ name: 'genericDuplicateExceptionRegex',
+ id: 'generic-duplicated-settings-regex-input',
});
});
it('on update event calls the mutation', async () => {
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
- mountComponent({ mutationResolver });
+ mountComponent({ mountFn: mountExtended, mutationResolver });
fillApolloCache();
- findMavenDuplicatedSettings().vm.$emit('update', {
+ findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', {
genericDuplicateExceptionRegex: ')',
});
@@ -176,9 +214,11 @@ describe('Packages Settings', () => {
describe('settings update', () => {
describe('success state', () => {
- it('emits a success event', async () => {
- mountComponent();
+ beforeEach(() => {
+ mountComponent({ mountFn: mountExtended });
+ });
+ it('emits a success event', async () => {
fillApolloCache();
emitMavenSettingsUpdate();
@@ -189,11 +229,12 @@ describe('Packages Settings', () => {
it('has an optimistic response', () => {
const mavenDuplicateExceptionRegex = 'latest[main]something';
- mountComponent();
fillApolloCache();
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe('');
+ expect(
+ findGenericDuplicatedSettingsExceptionsInput().props('duplicateExceptionRegex'),
+ ).toBe('');
emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex });
@@ -209,7 +250,7 @@ describe('Packages Settings', () => {
// note this is a complex test that covers all the path around errors that are shown in the form
// it's one single it case, due to the expensive preparation and execution
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock);
- mountComponent({ mutationResolver });
+ mountComponent({ mountFn: mountExtended, mutationResolver });
fillApolloCache();
@@ -218,9 +259,9 @@ describe('Packages Settings', () => {
await waitForPromises();
// errors are bound to the component
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(
- groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
- );
+ expect(
+ findMavenDuplicatedSettingsExceptionsInput().props('duplicateExceptionRegexError'),
+ ).toBe(groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message);
// general error message is shown
@@ -231,7 +272,9 @@ describe('Packages Settings', () => {
await nextTick();
// errors are reset on mutation call
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
+ expect(
+ findMavenDuplicatedSettingsExceptionsInput().props('duplicateExceptionRegexError'),
+ ).toBe('');
});
it.each`
@@ -239,7 +282,7 @@ describe('Packages Settings', () => {
${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))}
${'network'} | ${jest.fn().mockRejectedValue()}
`('mutation payload with $type error', async ({ mutationResolver }) => {
- mountComponent({ mutationResolver });
+ mountComponent({ mountFn: mountExtended, mutationResolver });
fillApolloCache();
emitMavenSettingsUpdate();
diff --git a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js b/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js
deleted file mode 100644
index fcfad4b42b8..00000000000
--- a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
-
-describe('settings_titles', () => {
- let wrapper;
-
- const defaultProps = {
- title: 'foo',
- subTitle: 'bar',
- };
-
- const mountComponent = (propsData = defaultProps) => {
- wrapper = shallowMount(SettingsTitles, {
- propsData,
- });
- };
-
- const findSubTitle = () => wrapper.find('p');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders properly', () => {
- mountComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('does not render the subtitle paragraph when no subtitle is passed', () => {
- mountComponent({ title: defaultProps.title });
-
- expect(findSubTitle().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
new file mode 100644
index 00000000000..8b60f31512b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
@@ -0,0 +1,164 @@
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import component from '~/packages_and_registries/settings/project/components/cleanup_image_tags.vue';
+import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
+import {
+ CONTAINER_CLEANUP_POLICY_TITLE,
+ CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ UNAVAILABLE_USER_FEATURE_TEXT,
+} from '~/packages_and_registries/settings/project/constants';
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+
+import {
+ expirationPolicyPayload,
+ emptyExpirationPolicyPayload,
+ containerExpirationPolicyData,
+} from '../mock_data';
+
+describe('Cleanup image tags project settings', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const defaultProvidedValues = {
+ projectPath: 'path',
+ isAdmin: false,
+ adminSettingsPath: 'settingsPath',
+ enableHistoricEntries: false,
+ helpPagePath: 'helpPagePath',
+ showCleanupPolicyLink: false,
+ };
+
+ const findFormComponent = () => wrapper.findComponent(ContainerExpirationPolicyForm);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findDescription = () => wrapper.findByTestId('description');
+
+ const mountComponent = (provide = defaultProvidedValues, config) => {
+ wrapper = shallowMountExtended(component, {
+ stubs: {
+ GlSprintf,
+ },
+ provide,
+ ...config,
+ });
+ };
+
+ const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[expirationPolicyQuery, resolver]];
+
+ fakeApollo = createMockApollo(requestHandlers);
+ mountComponent(provide, {
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('isEdited status', () => {
+ it.each`
+ description | apiResponse | workingCopy | result
+ ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
+ ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
+ ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
+ ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
+ ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
+ `('$description', async ({ apiResponse, workingCopy, result }) => {
+ mountComponentWithApollo({
+ provide: { ...defaultProvidedValues, enableHistoricEntries: true },
+ resolver: jest.fn().mockResolvedValue(apiResponse),
+ });
+ await waitForPromises();
+
+ findFormComponent().vm.$emit('input', workingCopy);
+
+ await waitForPromises();
+
+ expect(findFormComponent().props('isEdited')).toBe(result);
+ });
+ });
+
+ it('renders the setting form', async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
+ });
+ await waitForPromises();
+
+ expect(findFormComponent().exists()).toBe(true);
+ expect(findTitle().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_TITLE);
+ expect(findDescription().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_DESCRIPTION);
+ });
+
+ describe('the form is disabled', () => {
+ it('hides the form', () => {
+ mountComponent();
+
+ expect(findFormComponent().exists()).toBe(false);
+ });
+
+ it('shows an alert', () => {
+ mountComponent();
+
+ const text = findAlert().text();
+ expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
+ expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT);
+ });
+
+ describe('an admin is visiting the page', () => {
+ it('shows the admin part of the alert message', () => {
+ mountComponent({ ...defaultProvidedValues, isAdmin: true });
+
+ const sprintf = findAlert().findComponent(GlSprintf);
+ expect(sprintf.text()).toBe('administration settings');
+ expect(sprintf.findComponent(GlLink).attributes('href')).toBe(
+ defaultProvidedValues.adminSettingsPath,
+ );
+ });
+ });
+ });
+
+ describe('fetchSettingsError', () => {
+ beforeEach(async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ });
+ await waitForPromises();
+ });
+
+ it('hides the form', () => {
+ expect(findFormComponent().exists()).toBe(false);
+ });
+
+ it('shows an alert', () => {
+ expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
+ });
+ });
+
+ describe('empty API response', () => {
+ it.each`
+ enableHistoricEntries | isShown
+ ${true} | ${true}
+ ${false} | ${false}
+ `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
+ mountComponentWithApollo({
+ provide: {
+ ...defaultProvidedValues,
+ enableHistoricEntries,
+ },
+ resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
+ });
+ await waitForPromises();
+
+ expect(findFormComponent().exists()).toBe(isShown);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index ca44e77e694..8e08864bdb8 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -2,13 +2,11 @@ import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
-import {
- UPDATE_SETTINGS_ERROR_MESSAGE,
- UPDATE_SETTINGS_SUCCESS_MESSAGE,
-} from '~/packages_and_registries/settings/project/constants';
+import { UPDATE_SETTINGS_ERROR_MESSAGE } from '~/packages_and_registries/settings/project/constants';
import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import Tracking from '~/tracking';
@@ -20,6 +18,7 @@ describe('Container Expiration Policy Settings Form', () => {
const defaultProvidedValues = {
projectPath: 'path',
+ projectSettingsPath: 'settings-path',
};
const {
@@ -36,7 +35,7 @@ describe('Container Expiration Policy Settings Form', () => {
label: 'docker_container_retention_and_expiration_policies',
};
- const findForm = () => wrapper.find({ ref: 'form-element' });
+ const findForm = () => wrapper.find('form');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"');
const findSaveButton = () => wrapper.find('[data-testid="save-button"');
@@ -208,7 +207,9 @@ describe('Container Expiration Policy Settings Form', () => {
});
it('validation event updates buttons disabled state', async () => {
- mountComponent();
+ mountComponent({
+ props: { ...defaultProps, isEdited: true },
+ });
expect(findSaveButton().props('disabled')).toBe(false);
@@ -229,52 +230,22 @@ describe('Container Expiration Policy Settings Form', () => {
});
describe('form', () => {
- describe('form reset event', () => {
- it('calls the appropriate function', () => {
- mountComponent();
-
- findForm().trigger('reset');
-
- expect(wrapper.emitted('reset')).toEqual([[]]);
- });
-
- it('tracks the reset event', () => {
- mountComponent();
-
- findForm().trigger('reset');
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
- });
-
- it('resets the errors objects', async () => {
- mountComponent({
- data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } },
- });
-
- findForm().trigger('reset');
-
- await nextTick();
+ describe('form submit event', () => {
+ useMockLocationHelper();
- expect(findKeepRegexInput().props('error')).toBe('');
- expect(findRemoveRegexInput().props('error')).toBe('');
- expect(findSaveButton().props('disabled')).toBe(false);
- });
- });
-
- describe('form submit event ', () => {
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
- it('dispatches the correct apollo mutation', () => {
+ it('dispatches the correct apollo mutation', async () => {
const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload());
mountComponentWithApollo({
mutationResolver,
});
- findForm().trigger('submit');
+ await submitForm();
expect(mutationResolver).toHaveBeenCalled();
});
@@ -286,9 +257,7 @@ describe('Container Expiration Policy Settings Form', () => {
queryPayload: expirationPolicyPayload({ keepN: null, cadence: null, olderThan: null }),
});
- await waitForPromises();
-
- findForm().trigger('submit');
+ await submitForm();
expect(mutationResolver).toHaveBeenCalledWith({
input: {
@@ -303,24 +272,26 @@ describe('Container Expiration Policy Settings Form', () => {
});
});
- it('tracks the submit event', () => {
+ it('tracks the submit event', async () => {
mountComponentWithApollo({
mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()),
});
- findForm().trigger('submit');
+ await submitForm();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
});
- it('show a success toast when submit succeed', async () => {
+ it('redirects to package and registry project settings page when submitted successfully', async () => {
mountComponentWithApollo({
mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()),
});
await submitForm();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
+ expect(window.location.href.endsWith('settings-path?showSetupSuccessAlert=true')).toBe(
+ true,
+ );
});
describe('when submit fails', () => {
@@ -348,6 +319,7 @@ describe('Container Expiration Policy Settings Form', () => {
await submitForm();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
+ expect(window.location.href).toBeUndefined();
});
it('parses the error messages', async () => {
@@ -375,24 +347,24 @@ describe('Container Expiration Policy Settings Form', () => {
describe('form actions', () => {
describe('cancel button', () => {
- it('has type reset', () => {
+ it('links to project package and registry settings path', () => {
mountComponent();
- expect(findCancelButton().attributes('type')).toBe('reset');
+ expect(findCancelButton().attributes('href')).toBe(
+ defaultProvidedValues.projectSettingsPath,
+ );
});
it.each`
- isLoading | isEdited | mutationLoading
- ${true} | ${true} | ${true}
- ${false} | ${true} | ${true}
- ${false} | ${false} | ${true}
- ${true} | ${false} | ${false}
- ${false} | ${false} | ${false}
+ isLoading | mutationLoading
+ ${true} | ${true}
+ ${false} | ${true}
+ ${true} | ${false}
`(
- 'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled',
- ({ isEdited, isLoading, mutationLoading }) => {
+ 'is disabled when isLoading is $isLoading and mutationLoading is $mutationLoading',
+ ({ isLoading, mutationLoading }) => {
mountComponent({
- props: { ...defaultProps, isEdited, isLoading },
+ props: { ...defaultProps, isLoading },
data: { mutationLoading },
});
@@ -409,18 +381,19 @@ describe('Container Expiration Policy Settings Form', () => {
});
it.each`
- isLoading | localErrors | mutationLoading
- ${true} | ${{}} | ${true}
- ${true} | ${{}} | ${false}
- ${false} | ${{}} | ${true}
- ${false} | ${{ foo: false }} | ${true}
- ${true} | ${{ foo: false }} | ${false}
- ${false} | ${{ foo: false }} | ${false}
+ isLoading | isEdited | localErrors | mutationLoading
+ ${true} | ${false} | ${{}} | ${true}
+ ${true} | ${false} | ${{}} | ${false}
+ ${false} | ${false} | ${{}} | ${true}
+ ${false} | ${false} | ${{}} | ${false}
+ ${false} | ${false} | ${{ foo: false }} | ${true}
+ ${true} | ${false} | ${{ foo: false }} | ${false}
+ ${false} | ${false} | ${{ foo: false }} | ${false}
`(
- 'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled',
- ({ localErrors, isLoading, mutationLoading }) => {
+ 'is disabled when isLoading is $isLoading, isEdited is $isEdited, localErrors is $localErrors and mutationLoading is $mutationLoading',
+ ({ localErrors, isEdited, isLoading, mutationLoading }) => {
mountComponent({
- props: { ...defaultProps, isLoading },
+ props: { ...defaultProps, isEdited, isLoading },
data: { mutationLoading, localErrors },
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index d83c717da6a..35baeaeac61 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -1,12 +1,14 @@
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlSprintf, GlLink, GlCard } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
-import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
import {
+ CONTAINER_CLEANUP_POLICY_EDIT_RULES,
+ CONTAINER_CLEANUP_POLICY_SET_RULES,
+ CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
@@ -14,11 +16,7 @@ import {
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-import {
- expirationPolicyPayload,
- emptyExpirationPolicyPayload,
- containerExpirationPolicyData,
-} from '../mock_data';
+import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
describe('Container expiration policy project settings', () => {
let wrapper;
@@ -28,17 +26,19 @@ describe('Container expiration policy project settings', () => {
projectPath: 'path',
isAdmin: false,
adminSettingsPath: 'settingsPath',
+ cleanupSettingsPath: 'cleanupSettingsPath',
enableHistoricEntries: false,
helpPagePath: 'helpPagePath',
- showCleanupPolicyLink: false,
};
- const findFormComponent = () => wrapper.find(ContainerExpirationPolicyForm);
- const findAlert = () => wrapper.find(GlAlert);
- const findSettingsBlock = () => wrapper.find(SettingsBlock);
+ const findFormComponent = () => wrapper.findComponent(GlCard);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findButton = () => wrapper.findByTestId('rules-button');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const mountComponent = (provide = defaultProvidedValues, config) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
stubs: {
GlSprintf,
SettingsBlock,
@@ -63,37 +63,19 @@ describe('Container expiration policy project settings', () => {
wrapper.destroy();
});
- describe('isEdited status', () => {
- it.each`
- description | apiResponse | workingCopy | result
- ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
- ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
- ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
- ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
- ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
- `('$description', async ({ apiResponse, workingCopy, result }) => {
- mountComponentWithApollo({
- provide: { ...defaultProvidedValues, enableHistoricEntries: true },
- resolver: jest.fn().mockResolvedValue(apiResponse),
- });
- await waitForPromises();
-
- findFormComponent().vm.$emit('input', workingCopy);
-
- await waitForPromises();
-
- expect(findFormComponent().props('isEdited')).toBe(result);
- });
- });
-
it('renders the setting form', async () => {
mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
});
await waitForPromises();
- expect(findFormComponent().exists()).toBe(true);
expect(findSettingsBlock().exists()).toBe(true);
+ expect(findFormComponent().exists()).toBe(true);
+ expect(findDescription().text()).toMatchInterpolatedText(
+ CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
+ );
+ expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_EDIT_RULES);
+ expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
});
describe('the form is disabled', () => {
@@ -115,9 +97,9 @@ describe('Container expiration policy project settings', () => {
it('shows the admin part of the alert message', () => {
mountComponent({ ...defaultProvidedValues, isAdmin: true });
- const sprintf = findAlert().find(GlSprintf);
+ const sprintf = findAlert().findComponent(GlSprintf);
expect(sprintf.text()).toBe('administration settings');
- expect(sprintf.find(GlLink).attributes('href')).toBe(
+ expect(sprintf.findComponent(GlLink).attributes('href')).toBe(
defaultProvidedValues.adminSettingsPath,
);
});
@@ -157,6 +139,10 @@ describe('Container expiration policy project settings', () => {
await waitForPromises();
expect(findFormComponent().exists()).toBe(isShown);
+ if (isShown) {
+ expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_SET_RULES);
+ expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
+ }
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
index 8b99ac6b06c..ae41fdf65e0 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -14,8 +14,8 @@ describe('ExpirationDropdown', () => {
],
};
- const findFormSelect = () => wrapper.find(GlFormSelect);
- const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findFormSelect = () => wrapper.findComponent(GlFormSelect);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findDescription = () => wrapper.find('[data-testid="description"]');
const findOptions = () => wrapper.findAll('[data-testid="option"]');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
index 6b681924fcf..1cea0704154 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
@@ -16,11 +16,11 @@ describe('ExpirationInput', () => {
const tagsRegexHelpPagePath = 'fooPath';
- const findInput = () => wrapper.find(GlFormInput);
- const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findLabel = () => wrapper.find('[data-testid="label"]');
const findDescription = () => wrapper.find('[data-testid="description"]');
- const findDescriptionLink = () => wrapper.find(GlLink);
+ const findDescriptionLink = () => wrapper.findComponent(GlLink);
const mountComponent = (props) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
index 94f7783afe7..653f2a8b40e 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
@@ -11,8 +11,8 @@ describe('ExpirationToggle', () => {
let wrapper;
const value = 'foo';
- const findInput = () => wrapper.find(GlFormInput);
- const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const mountComponent = (propsData) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
index 45039614e49..55a66cebd83 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
@@ -10,7 +10,7 @@ import {
describe('ExpirationToggle', () => {
let wrapper;
- const findToggle = () => wrapper.find(GlToggle);
+ const findToggle = () => wrapper.findComponent(GlToggle);
const findDescription = () => wrapper.find('[data-testid="description"]');
const mountComponent = (propsData) => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
index 86f45d78bae..daf0ee85fdf 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
@@ -39,7 +39,7 @@ describe('Packages Cleanup Policy Settings Form', () => {
label: 'packages_cleanup_policies',
};
- const findForm = () => wrapper.find({ ref: 'form-element' });
+ const findForm = () => wrapper.findComponent({ ref: 'form-element' });
const findSaveButton = () => wrapper.findByTestId('save-button');
const findKeepNDuplicatedPackageFilesDropdown = () =>
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index f576bc79eae..07d13839c61 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -1,41 +1,99 @@
+import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import * as commonUtils from '~/lib/utils/common_utils';
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
+import {
+ SHOW_SETUP_SUCCESS_ALERT,
+ UPDATE_SETTINGS_SUCCESS_MESSAGE,
+} from '~/packages_and_registries/settings/project/constants';
+
+jest.mock('~/lib/utils/common_utils');
describe('Registry Settings app', () => {
let wrapper;
- const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
- const findPackagesCleanupPolicy = () => wrapper.find(PackagesCleanupPolicy);
+ const findContainerExpirationPolicy = () => wrapper.findComponent(ContainerExpirationPolicy);
+ const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy);
+ const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- const mountComponent = (provide) => {
+ const defaultProvide = {
+ showContainerRegistrySettings: true,
+ showPackageRegistrySettings: true,
+ };
+
+ const mountComponent = (provide = defaultProvide) => {
wrapper = shallowMount(component, {
provide,
});
};
- it.each`
- showContainerRegistrySettings | showPackageRegistrySettings
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${true}
- ${false} | ${false}
- `(
- 'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
- ({ showContainerRegistrySettings, showPackageRegistrySettings }) => {
- mountComponent({
- showContainerRegistrySettings,
- showPackageRegistrySettings,
+ describe('container policy success alert handling', () => {
+ const originalLocation = window.location.href;
+ const search = `?${SHOW_SETUP_SUCCESS_ALERT}=true`;
+
+ beforeEach(() => {
+ setWindowLocation(search);
+ });
+
+ afterEach(() => {
+ setWindowLocation(originalLocation);
+ });
+
+ it(`renders alert if the query string contains ${SHOW_SETUP_SUCCESS_ALERT}`, async () => {
+ mountComponent();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().props()).toMatchObject({
+ dismissible: true,
+ variant: 'success',
});
+ expect(findAlert().text()).toMatchInterpolatedText(UPDATE_SETTINGS_SUCCESS_MESSAGE);
+ });
+
+ it('calls historyReplaceState with a clean url', () => {
+ mountComponent();
+
+ expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation);
+ });
+
+ it(`does nothing if the query string does not contain ${SHOW_SETUP_SUCCESS_ALERT}`, () => {
+ setWindowLocation('?');
+ mountComponent();
- expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings);
- expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings);
- },
- );
+ expect(findAlert().exists()).toBe(false);
+ expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('settings', () => {
+ it.each`
+ showContainerRegistrySettings | showPackageRegistrySettings
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${true}
+ ${false} | ${false}
+ `(
+ 'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
+ ({ showContainerRegistrySettings, showPackageRegistrySettings }) => {
+ mountComponent({
+ showContainerRegistrySettings,
+ showPackageRegistrySettings,
+ });
+
+ expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings);
+ expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings);
+ },
+ );
+ });
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
index 7727bf167fe..18084766db9 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
@@ -15,15 +15,15 @@ import {
import Tracking from '~/tracking';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
-import { dockerCommands } from '../../mock_data';
+import { dockerCommands } from 'jest/packages_and_registries/container_registry/explorer/mock_data';
Vue.use(Vuex);
describe('cli_commands', () => {
let wrapper;
- const findDropdownButton = () => wrapper.find(GlDropdown);
- const findCodeInstruction = () => wrapper.findAll(CodeInstruction);
+ const findDropdownButton = () => wrapper.findComponent(GlDropdown);
+ const findCodeInstruction = () => wrapper.findAllComponents(CodeInstruction);
const mountComponent = () => {
wrapper = mount(QuickstartDropdown, {
diff --git a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
index d6d1970cb12..a0ff6ca01b5 100644
--- a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
@@ -5,7 +5,7 @@ import PackageIconAndName from '~/packages_and_registries/shared/components/pack
describe('PackageIconAndName', () => {
let wrapper;
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const mountComponent = () => {
wrapper = shallowMount(PackageIconAndName, {
diff --git a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
index 3a52c243867..3c512cfd6ae 100644
--- a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
+++ b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
@@ -48,7 +48,7 @@ describe('UsageStatistics', () => {
expectEnabledservicePingFeaturesCheckBox();
});
- it('is switched to disabled when Service Ping checkbox is unchecked ', () => {
+ it('is switched to disabled when Service Ping checkbox is unchecked', () => {
servicePingCheckBox.click();
servicePingFeaturesCheckBox.click();
expectEnabledservicePingFeaturesCheckBox();
diff --git a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
index 7a8a249cb2a..b020caa3010 100644
--- a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
+++ b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
@@ -14,7 +14,7 @@ describe('BitbucketServerStatusTable', () => {
const findReconfigureButton = () =>
wrapper
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.filter((w) => w.props().variant === 'info')
.at(0);
@@ -36,7 +36,7 @@ describe('BitbucketServerStatusTable', () => {
it('renders bitbucket status table component', () => {
createComponent();
- expect(wrapper.find(BitbucketStatusTable).exists()).toBe(true);
+ expect(wrapper.findComponent(BitbucketStatusTable).exists()).toBe(true);
});
it('renders Reconfigure button', async () => {
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index a850b1655f7..1790a9c9bf5 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -84,7 +84,7 @@ describe('BulkImportsHistoryApp', () => {
describe('general behavior', () => {
it('renders loading state when loading', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders empty state when no data is available', async () => {
@@ -92,8 +92,8 @@ describe('BulkImportsHistoryApp', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('renders table with data when history is available', async () => {
@@ -101,7 +101,7 @@ describe('BulkImportsHistoryApp', () => {
createComponent();
await axios.waitForAll();
- const table = wrapper.find(GlTable);
+ const table = wrapper.findComponent(GlTable);
expect(table.exists()).toBe(true);
// can't use .props() or .attributes() here
expect(table.vm.$attrs.items).toHaveLength(DUMMY_RESPONSE.length);
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
index 4ff3f0361cf..82a3e11186e 100644
--- a/spec/frontend/pages/import/history/components/import_error_details_spec.js
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -41,7 +41,7 @@ describe('ImportErrorDetails', () => {
describe('general behavior', () => {
it('renders loading state when loading', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders import_error if it is available', async () => {
@@ -50,7 +50,7 @@ describe('ImportErrorDetails', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('pre').text()).toBe(FAKE_IMPORT_ERROR);
});
@@ -59,7 +59,7 @@ describe('ImportErrorDetails', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('pre').text()).toBe('No additional information provided.');
});
});
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
index 0d821b114cf..5030adae2fa 100644
--- a/spec/frontend/pages/import/history/components/import_history_app_spec.js
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -79,7 +79,7 @@ describe('ImportHistoryApp', () => {
describe('general behavior', () => {
it('renders loading state when loading', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders empty state when no data is available', async () => {
@@ -87,8 +87,8 @@ describe('ImportHistoryApp', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('renders table with data when history is available', async () => {
@@ -96,7 +96,7 @@ describe('ImportHistoryApp', () => {
createComponent();
await axios.waitForAll();
- const table = wrapper.find(GlTable);
+ const table = wrapper.findComponent(GlTable);
expect(table.exists()).toBe(true);
expect(table.props().items).toStrictEqual(DUMMY_RESPONSE);
});
@@ -127,7 +127,7 @@ describe('ImportHistoryApp', () => {
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE }));
- expect(wrapper.find(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY);
+ expect(wrapper.findComponent(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY);
});
});
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
deleted file mode 100644
index fa6e7e51a60..00000000000
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import $ from 'jquery';
-import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import EmojiMenu from '~/pages/profiles/show/emoji_menu';
-
-describe('EmojiMenu', () => {
- const dummyEmojiTag = '<dummy></tag>';
- const dummyToggleButtonSelector = '.toggle-button-selector';
- const dummyMenuClass = 'dummy-menu-class';
-
- let emojiMenu;
- let dummySelectEmojiCallback;
- let dummyEmojiList;
-
- beforeEach(() => {
- dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback');
- dummyEmojiList = {
- glEmojiTag() {
- return dummyEmojiTag;
- },
- normalizeEmojiName(emoji) {
- return emoji;
- },
- isEmojiNameValid() {
- return true;
- },
- getEmojiCategoryMap() {
- return { dummyCategory: [] };
- },
- };
-
- emojiMenu = new EmojiMenu(
- dummyEmojiList,
- dummyToggleButtonSelector,
- dummyMenuClass,
- dummySelectEmojiCallback,
- );
- });
-
- afterEach(() => {
- emojiMenu.destroy();
- });
-
- describe('addAward', () => {
- const dummyAwardUrl = `${TEST_HOST}/award/url`;
- const dummyEmoji = 'tropical_fish';
- const dummyVotesBlock = () => $('<div />');
-
- it('calls selectEmojiCallback', async () => {
- expect(dummySelectEmojiCallback).not.toHaveBeenCalled();
-
- await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false);
- expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag);
- });
-
- it('does not make an axios request', async () => {
- jest.spyOn(axios, 'request').mockReturnValue();
-
- await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false);
- expect(axios.request).not.toHaveBeenCalled();
- });
- });
-
- describe('bindEvents', () => {
- beforeEach(() => {
- jest.spyOn(emojiMenu, 'registerEventListener').mockReturnValue();
- });
-
- it('binds event listeners to custom toggle button', () => {
- emojiMenu.bindEvents();
-
- expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
- 'one',
- expect.anything(),
- 'mouseenter focus',
- dummyToggleButtonSelector,
- 'mouseenter focus',
- expect.anything(),
- );
-
- expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
- 'on',
- expect.anything(),
- 'click',
- dummyToggleButtonSelector,
- expect.anything(),
- );
- });
-
- it('binds event listeners to custom menu class', () => {
- emojiMenu.bindEvents();
-
- expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
- 'on',
- expect.anything(),
- 'click',
- `.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
- expect.anything(),
- );
- });
- });
-
- describe('createEmojiMenu', () => {
- it('renders the menu with custom menu class', () => {
- const menuElement = () =>
- document.body.querySelector(`.emoji-menu.${dummyMenuClass} .emoji-menu-content`);
-
- expect(menuElement()).toBe(null);
-
- emojiMenu.createEmojiMenu();
-
- expect(menuElement()).not.toBe(null);
- });
- });
-});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 2a0fde45384..f221a90da61 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -4,11 +4,14 @@ import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import createFlash from '~/flash';
-import httpStatus from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
+import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
jest.mock('~/flash');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -16,6 +19,7 @@ jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('ForkForm component', () => {
let wrapper;
let axiosMock;
+ let mockQueryResponse;
const PROJECT_VISIBILITY_TYPE = {
private:
@@ -24,26 +28,11 @@ describe('ForkForm component', () => {
public: 'Public The project can be accessed without any authentication.',
};
- const GON_GITLAB_URL = 'https://gitlab.com';
const GON_API_VERSION = 'v7';
- const MOCK_NAMESPACES_RESPONSE = [
- {
- name: 'one',
- full_name: 'one-group/one',
- id: 1,
- },
- {
- name: 'two',
- full_name: 'two-group/two',
- id: 2,
- },
- ];
-
const DEFAULT_PROVIDE = {
newGroupPath: 'some/groups/path',
visibilityHelpPath: 'some/visibility/help/path',
- endpoint: '/some/project-full-path/-/forks/new.json',
projectFullPath: '/some/project-full-path',
projectId: '10',
projectName: 'Project Name',
@@ -53,12 +42,44 @@ describe('ForkForm component', () => {
restrictedVisibilityLevels: [],
};
- const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => {
- axiosMock.onGet(DEFAULT_PROVIDE.endpoint).replyOnce(statusCode, data);
- };
+ Vue.use(VueApollo);
const createComponentFactory = (mountFn) => (provide = {}, data = {}) => {
+ const queryResponse = {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ forkTargets: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/21',
+ fullPath: 'flightjs',
+ name: 'Flight JS',
+ visibility: 'public',
+ },
+ {
+ id: 'gid://gitlab/Namespace/4',
+ fullPath: 'root',
+ name: 'Administrator',
+ visibility: 'public',
+ },
+ ],
+ },
+ },
+ };
+
+ mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse });
+ const requestHandlers = [[searchQuery, mockQueryResponse]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: searchQuery,
+ data: {
+ ...queryResponse,
+ },
+ });
+
wrapper = mountFn(ForkForm, {
+ apolloProvider,
provide: {
...DEFAULT_PROVIDE,
...provide,
@@ -83,7 +104,6 @@ describe('ForkForm component', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
window.gon = {
- gitlab_url: GON_GITLAB_URL,
api_version: GON_API_VERSION,
};
});
@@ -93,12 +113,11 @@ describe('ForkForm component', () => {
axiosMock.restore();
});
- const findFormSelectOptions = () => wrapper.find('select[name="namespace"]').findAll('option');
const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]');
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]');
- const findForkUrlInput = () => wrapper.find('[data-testid="fork-url-input"]');
+ const findForkUrlInput = () => wrapper.findComponent(ProjectNamespace);
const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]');
const findForkDescriptionTextarea = () =>
wrapper.find('[data-testid="fork-description-textarea"]');
@@ -106,7 +125,6 @@ describe('ForkForm component', () => {
wrapper.find('[data-testid="fork-visibility-radio-group"]');
it('will go to projectFullPath when click cancel button', () => {
- mockGetRequest();
createComponent();
const { projectFullPath } = DEFAULT_PROVIDE;
@@ -115,8 +133,13 @@ describe('ForkForm component', () => {
expect(cancelButton.attributes('href')).toBe(projectFullPath);
});
+ const selectedMockNamespace = { name: 'two', full_name: 'two-group/two', id: 2 };
+
+ const fillForm = () => {
+ findForkUrlInput().vm.$emit('select', selectedMockNamespace);
+ };
+
it('has input with csrf token', () => {
- mockGetRequest();
createComponent();
expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe(
@@ -125,7 +148,6 @@ describe('ForkForm component', () => {
});
it('pre-populate form from project props', () => {
- mockGetRequest();
createComponent();
expect(findForkNameInput().attributes('value')).toBe(DEFAULT_PROVIDE.projectName);
@@ -135,75 +157,19 @@ describe('ForkForm component', () => {
);
});
- it('sets project URL prepend text with gon.gitlab_url', () => {
- mockGetRequest();
- createComponent();
-
- expect(wrapper.find(GlFormInputGroup).text()).toContain(`${GON_GITLAB_URL}/`);
- });
-
it('will have required attribute for required fields', () => {
- mockGetRequest();
createComponent();
expect(findForkNameInput().attributes('required')).not.toBeUndefined();
- expect(findForkUrlInput().attributes('required')).not.toBeUndefined();
expect(findForkSlugInput().attributes('required')).not.toBeUndefined();
expect(findVisibilityRadioGroup().attributes('required')).not.toBeUndefined();
expect(findForkDescriptionTextarea().attributes('required')).toBeUndefined();
});
- describe('forks namespaces', () => {
- beforeEach(() => {
- mockGetRequest({ namespaces: MOCK_NAMESPACES_RESPONSE });
- createFullComponent();
- });
-
- it('make GET request from endpoint', async () => {
- await axios.waitForAll();
-
- expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROVIDE.endpoint);
- });
-
- it('generate default option', async () => {
- await axios.waitForAll();
-
- const optionsArray = findForkUrlInput().findAll('option');
-
- expect(optionsArray.at(0).text()).toBe('Select a namespace');
- });
-
- it('populate project url namespace options', async () => {
- await axios.waitForAll();
-
- const optionsArray = findForkUrlInput().findAll('option');
-
- expect(optionsArray).toHaveLength(MOCK_NAMESPACES_RESPONSE.length + 1);
- expect(optionsArray.at(1).text()).toBe(MOCK_NAMESPACES_RESPONSE[0].full_name);
- expect(optionsArray.at(2).text()).toBe(MOCK_NAMESPACES_RESPONSE[1].full_name);
- });
-
- it('set namespaces in alphabetical order', async () => {
- const namespace = {
- name: 'three',
- full_name: 'aaa/three',
- id: 3,
- };
- mockGetRequest({
- namespaces: [...MOCK_NAMESPACES_RESPONSE, namespace],
- });
- createComponent();
- await axios.waitForAll();
-
- expect(wrapper.vm.namespaces).toEqual([namespace, ...MOCK_NAMESPACES_RESPONSE]);
- });
- });
-
describe('project slug', () => {
const projectPath = 'some other project slug';
beforeEach(() => {
- mockGetRequest();
createComponent({
projectPath,
});
@@ -232,10 +198,9 @@ describe('ForkForm component', () => {
describe('visibility level', () => {
it('displays the correct description', () => {
- mockGetRequest();
createComponent();
- const formRadios = wrapper.findAll(GlFormRadio);
+ const formRadios = wrapper.findAllComponents(GlFormRadio);
Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibilityType, index) => {
expect(formRadios.at(index).text()).toBe(PROJECT_VISIBILITY_TYPE[visibilityType]);
@@ -243,10 +208,9 @@ describe('ForkForm component', () => {
});
it('displays all 3 visibility levels', () => {
- mockGetRequest();
createComponent();
- expect(wrapper.findAll(GlFormRadio)).toHaveLength(3);
+ expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(3);
});
describe('when the namespace is changed', () => {
@@ -262,16 +226,12 @@ describe('ForkForm component', () => {
},
];
- beforeEach(() => {
- mockGetRequest();
- });
-
it('resets the visibility to default "private"', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
- await findFormSelectOptions().at(1).setSelected();
+ fillForm();
await nextTick();
expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
@@ -280,8 +240,7 @@ describe('ForkForm component', () => {
it('sets the visibility to be null when restrictedVisibilityLevels is set', async () => {
createFullComponent({ restrictedVisibilityLevels: [10] }, { namespaces });
- await findFormSelectOptions().at(1).setSelected();
-
+ fillForm();
await nextTick();
const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i });
@@ -315,8 +274,7 @@ describe('ForkForm component', () => {
${'public'} | ${[0, 20]}
${'public'} | ${[10, 20]}
${'public'} | ${[0, 10, 20]}
- `('checks the correct radio button', async ({ project, restrictedVisibilityLevels }) => {
- mockGetRequest();
+ `('checks the correct radio button', ({ project, restrictedVisibilityLevels }) => {
createFullComponent({
projectVisibility: project,
restrictedVisibilityLevels,
@@ -357,7 +315,7 @@ describe('ForkForm component', () => {
${'public'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
`(
'sets appropriate radio button disabled state',
- async ({
+ ({
project,
namespace,
privateIsDisabled,
@@ -365,7 +323,6 @@ describe('ForkForm component', () => {
publicIsDisabled,
restrictedVisibilityLevels,
}) => {
- mockGetRequest();
createComponent(
{
projectVisibility: project,
@@ -387,11 +344,9 @@ describe('ForkForm component', () => {
const setupComponent = (fields = {}) => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
- mockGetRequest();
createFullComponent(
{},
{
- namespaces: MOCK_NAMESPACES_RESPONSE,
form: {
state: true,
...fields,
@@ -400,25 +355,21 @@ describe('ForkForm component', () => {
);
};
- const selectedMockNamespaceIndex = 1;
- const namespaceId = MOCK_NAMESPACES_RESPONSE[selectedMockNamespaceIndex].id;
-
- const fillForm = async () => {
- const namespaceOptions = findForkUrlInput().findAll('option');
-
- await namespaceOptions.at(selectedMockNamespaceIndex + 1).setSelected();
- };
+ beforeEach(() => {
+ setupComponent();
+ });
const submitForm = async () => {
- await fillForm();
- const form = wrapper.find(GlForm);
+ fillForm();
+ await nextTick();
+ const form = wrapper.findComponent(GlForm);
await form.trigger('submit');
await nextTick();
};
describe('with invalid form', () => {
- it('does not make POST request', async () => {
+ it('does not make POST request', () => {
jest.spyOn(axios, 'post');
setupComponent();
@@ -471,7 +422,7 @@ describe('ForkForm component', () => {
description: projectDescription,
id: projectId,
name: projectName,
- namespace_id: namespaceId,
+ namespace_id: selectedMockNamespace.id,
path: projectPath,
visibility: projectVisibility,
};
diff --git a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
new file mode 100644
index 00000000000..1a88aebae32
--- /dev/null
+++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
@@ -0,0 +1,177 @@
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ GlTruncate,
+} from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import createFlash from '~/flash';
+import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
+import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
+
+jest.mock('~/flash');
+
+describe('ProjectNamespace component', () => {
+ let wrapper;
+ let originalGon;
+
+ const data = {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ forkTargets: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/21',
+ fullPath: 'flightjs',
+ name: 'Flight JS',
+ visibility: 'public',
+ },
+ {
+ id: 'gid://gitlab/Namespace/4',
+ fullPath: 'root',
+ name: 'Administrator',
+ visibility: 'public',
+ },
+ ],
+ },
+ },
+ };
+
+ const mockQueryResponse = jest.fn().mockResolvedValue({ data });
+
+ const emptyQueryResponse = {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ forkTargets: {
+ nodes: [],
+ },
+ },
+ };
+
+ const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error'));
+
+ Vue.use(VueApollo);
+
+ const gitlabUrl = 'https://gitlab.com';
+
+ const defaultProvide = {
+ projectFullPath: 'gitlab-org/project',
+ };
+
+ const mountComponent = ({
+ provide = defaultProvide,
+ queryHandler = mockQueryResponse,
+ mountFn = shallowMount,
+ } = {}) => {
+ const requestHandlers = [[searchQuery, queryHandler]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = mountFn(ProjectNamespace, {
+ apolloProvider,
+ provide,
+ });
+ };
+
+ const findButtonLabel = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownText = () => wrapper.findComponent(GlTruncate);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const clickDropdownItem = async () => {
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ await nextTick();
+ };
+
+ const showDropdown = () => {
+ findDropdown().vm.$emit('shown');
+ };
+
+ beforeAll(() => {
+ originalGon = window.gon;
+ window.gon = { gitlab_url: gitlabUrl };
+ });
+
+ afterAll(() => {
+ window.gon = originalGon;
+ wrapper.destroy();
+ });
+
+ describe('Initial state', () => {
+ beforeEach(() => {
+ mountComponent({ mountFn: mount });
+ jest.runOnlyPendingTimers();
+ });
+
+ it('renders the root url as a label', () => {
+ expect(findButtonLabel().text()).toBe(`${gitlabUrl}/`);
+ expect(findButtonLabel().props('label')).toBe(true);
+ });
+
+ it('renders placeholder text', () => {
+ expect(findDropdownText().props('text')).toBe('Select a namespace');
+ });
+ });
+
+ describe('After user interactions', () => {
+ beforeEach(async () => {
+ mountComponent({ mountFn: mount });
+ jest.runOnlyPendingTimers();
+ await nextTick();
+ showDropdown();
+ });
+
+ it('focuses on the input when the dropdown is opened', () => {
+ const spy = jest.spyOn(findInput().vm, 'focusInput');
+ showDropdown();
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('displays fetched namespaces', () => {
+ const listItems = wrapper.findAll('li');
+ expect(listItems).toHaveLength(3);
+ expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Namespaces');
+ expect(listItems.at(1).text()).toBe(data.project.forkTargets.nodes[0].fullPath);
+ expect(listItems.at(2).text()).toBe(data.project.forkTargets.nodes[1].fullPath);
+ });
+
+ it('sets the selected namespace', async () => {
+ const { fullPath } = data.project.forkTargets.nodes[0];
+ await clickDropdownItem();
+ expect(findDropdownText().props('text')).toBe(fullPath);
+ });
+ });
+
+ describe('With empty query response', () => {
+ beforeEach(() => {
+ mountComponent({ queryHandler: emptyQueryResponse, mountFn: mount });
+ jest.runOnlyPendingTimers();
+ });
+
+ it('renders `No matches found`', () => {
+ expect(wrapper.find('li').text()).toBe('No matches found');
+ });
+ });
+
+ describe('With error while fetching data', () => {
+ beforeEach(async () => {
+ mountComponent({ queryHandler: mockQueryError });
+ jest.runOnlyPendingTimers();
+ await nextTick();
+ });
+
+ it('creates a flash message and captures the error', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while loading data. Please refresh the page to try again.',
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index f272891919d..2f2edd6b025 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -20,9 +20,9 @@ describe('Code Coverage', () => {
const graphRef = 'master';
const graphCsvPath = 'url/';
- const findAlert = () => wrapper.find(GlAlert);
- const findAreaChart = () => wrapper.find(GlAreaChart);
- const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAreaChart = () => wrapper.findComponent(GlAreaChart);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
const findDownloadButton = () => wrapper.find('[data-testid="download-button"]');
@@ -142,7 +142,7 @@ describe('Code Coverage', () => {
});
it('renders the dropdown with all custom names as options', () => {
- expect(wrapper.find(GlDropdown).exists()).toBeDefined();
+ expect(wrapper.findComponent(GlDropdown).exists()).toBeDefined();
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
});
diff --git a/spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js b/spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js
new file mode 100644
index 00000000000..72077038dff
--- /dev/null
+++ b/spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js
@@ -0,0 +1,59 @@
+import { setHTMLFixture, resetHTMLFixture } from 'jest/__helpers__/fixtures';
+import initFormUpdate from '~/pages/projects/merge_requests/edit/update_form';
+
+describe('Update form state', () => {
+ const submitEvent = new Event('submit', {
+ bubbles: true,
+ cancelable: true,
+ });
+
+ const submitForm = () => document.querySelector('.merge-request-form').dispatchEvent(submitEvent);
+ const hiddenInputs = () => document.querySelectorAll('input[type="hidden"]');
+ const checkboxes = () => document.querySelectorAll('.js-form-update');
+
+ beforeEach(() => {
+ setHTMLFixture(`
+ <form class="merge-request-form">
+ <div class="form-check">
+ <input type="hidden" name="merge_request[force_remove_source_branch]" value="0" autocomplete="off">
+ <input type="checkbox" name="merge_request[force_remove_source_branch]" id="merge_request_force_remove_source_branch" value="1" class="form-check-input js-form-update">
+ </div>
+ <div class="form-check">
+ <input type="hidden" name="merge_request[squash]" value="0" autocomplete="off">
+ <input type="checkbox" name="merge_request[squash]" id="merge_request_squash" value="1" class="form-check-input js-form-update">
+ </div>
+ </form>`);
+ initFormUpdate();
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('at initial state', () => {
+ submitForm();
+ expect(hiddenInputs()).toHaveLength(2);
+ });
+
+ it('when one element is checked', () => {
+ checkboxes()[0].setAttribute('checked', true);
+ submitForm();
+ expect(hiddenInputs()).toHaveLength(1);
+ });
+
+ it('when all elements are checked', () => {
+ checkboxes()[0].setAttribute('checked', true);
+ checkboxes()[1].setAttribute('checked', true);
+ submitForm();
+ expect(hiddenInputs()).toHaveLength(0);
+ });
+
+ it('when checked and then unchecked', () => {
+ checkboxes()[0].setAttribute('checked', true);
+ checkboxes()[0].removeAttribute('checked');
+ checkboxes()[1].setAttribute('checked', true);
+ checkboxes()[1].removeAttribute('checked');
+ submitForm();
+ expect(hiddenInputs()).toHaveLength(2);
+ });
+});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index ca7f70f4434..a633332ab65 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -21,7 +21,7 @@ describe('Pipeline Schedule Callout', () => {
};
const findInnerContentOfCallout = () => wrapper.find('[data-testid="innerContent"]');
- const findDismissCalloutBtn = () => wrapper.find(GlButton);
+ const findDismissCalloutBtn = () => wrapper.findComponent(GlButton);
describe(`when ${cookieKey} cookie is set`, () => {
beforeEach(async () => {
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index f908508c4b5..ed7d4ad269e 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -5,8 +5,12 @@ import settingsPanel from '~/pages/projects/shared/permissions/components/settin
import {
featureAccessLevel,
visibilityLevelDescriptions,
- visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
+import {
+ VISIBILITY_LEVEL_PRIVATE_INTEGER,
+ VISIBILITY_LEVEL_INTERNAL_INTEGER,
+ VISIBILITY_LEVEL_PUBLIC_INTEGER,
+} from '~/visibility_level/constants';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
const defaultProps = {
@@ -81,15 +85,17 @@ describe('Settings Panel', () => {
});
};
- const findLFSSettingsRow = () => wrapper.find({ ref: 'git-lfs-settings' });
+ const findLFSSettingsRow = () => wrapper.findComponent({ ref: 'git-lfs-settings' });
const findLFSSettingsMessage = () => findLFSSettingsRow().find('p');
- const findLFSFeatureToggle = () => findLFSSettingsRow().find(GlToggle);
- const findRepositoryFeatureProjectRow = () => wrapper.find({ ref: 'repository-settings' });
+ const findLFSFeatureToggle = () => findLFSSettingsRow().findComponent(GlToggle);
+ const findRepositoryFeatureProjectRow = () =>
+ wrapper.findComponent({ ref: 'repository-settings' });
const findRepositoryFeatureSetting = () =>
- findRepositoryFeatureProjectRow().find(ProjectFeatureSetting);
- const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' });
- const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' });
- const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' });
+ findRepositoryFeatureProjectRow().findComponent(ProjectFeatureSetting);
+ const findProjectVisibilitySettings = () =>
+ wrapper.findComponent({ ref: 'project-visibility-settings' });
+ const findIssuesSettingsRow = () => wrapper.findComponent({ ref: 'issues-settings' });
+ const findAnalyticsRow = () => wrapper.findComponent({ ref: 'analytics-settings' });
const findProjectVisibilityLevelInput = () => wrapper.find('[name="project[visibility_level]"]');
const findRequestAccessEnabledInput = () =>
wrapper.find('[name="project[request_access_enabled]"]');
@@ -99,35 +105,40 @@ describe('Settings Panel', () => {
wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]');
const findBuildsAccessLevelInput = () =>
wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]');
- const findContainerRegistrySettings = () => wrapper.find({ ref: 'container-registry-settings' });
+ const findContainerRegistrySettings = () =>
+ wrapper.findComponent({ ref: 'container-registry-settings' });
const findContainerRegistryPublicNoteGlSprintfComponent = () =>
findContainerRegistrySettings().findComponent(GlSprintf);
const findContainerRegistryAccessLevelInput = () =>
wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]');
- const findPackageSettings = () => wrapper.find({ ref: 'package-settings' });
+ const findPackageSettings = () => wrapper.findComponent({ ref: 'package-settings' });
const findPackageAccessLevel = () =>
wrapper.find('[data-testid="package-registry-access-level"]');
const findPackageAccessLevels = () =>
wrapper.find('[name="project[project_feature_attributes][package_registry_access_level]"]');
const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
- const findPagesSettings = () => wrapper.find({ ref: 'pages-settings' });
+ const findPagesSettings = () => wrapper.findComponent({ ref: 'pages-settings' });
const findPagesAccessLevels = () =>
wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]');
- const findEmailSettings = () => wrapper.find({ ref: 'email-settings' });
+ const findEmailSettings = () => wrapper.findComponent({ ref: 'email-settings' });
const findShowDefaultAwardEmojis = () =>
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
const findWarnAboutPuc = () =>
wrapper.find(
'input[name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"]',
);
- const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
+ const findMetricsVisibilitySettings = () =>
+ wrapper.findComponent({ ref: 'metrics-visibility-settings' });
const findMetricsVisibilityInput = () =>
findMetricsVisibilitySettings().findComponent(ProjectFeatureSetting);
- const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
+ const findOperationsSettings = () => wrapper.findComponent({ ref: 'operations-settings' });
const findOperationsVisibilityInput = () =>
findOperationsSettings().findComponent(ProjectFeatureSetting);
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
+ const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' });
+ const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
+ const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
afterEach(() => {
wrapper.destroy();
@@ -156,13 +167,13 @@ describe('Settings Panel', () => {
});
it.each`
- option | allowedOptions | disabled
- ${visibilityOptions.PRIVATE} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
- ${visibilityOptions.PRIVATE} | ${[visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${true}
- ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
- ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.PUBLIC]} | ${true}
- ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false}
- ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL]} | ${true}
+ option | allowedOptions | disabled
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${true}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${true}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER]} | ${true}
`(
'sets disabled to $disabled for the visibility option $option when given $allowedOptions',
({ option, allowedOptions, disabled }) => {
@@ -181,35 +192,37 @@ describe('Settings Panel', () => {
it('should set the visibility level description based upon the selected visibility level', () => {
wrapper = mountComponent({ stubs: { GlSprintf } });
- findProjectVisibilityLevelInput().setValue(visibilityOptions.INTERNAL);
+ findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_INTERNAL_INTEGER);
expect(findProjectVisibilitySettings().text()).toContain(
- visibilityLevelDescriptions[visibilityOptions.INTERNAL],
+ visibilityLevelDescriptions[VISIBILITY_LEVEL_INTERNAL_INTEGER],
);
});
it('should show the request access checkbox if the visibility level is not private', () => {
wrapper = mountComponent({
- currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER },
});
expect(findRequestAccessEnabledInput().exists()).toBe(true);
});
it('should not show the request access checkbox if the visibility level is private', () => {
- wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } });
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER },
+ });
expect(findRequestAccessEnabledInput().exists()).toBe(false);
});
it('does not require confirmation if the visibility is reduced', async () => {
wrapper = mountComponent({
- currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER },
});
expect(findConfirmDangerButton().exists()).toBe(false);
- await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER);
expect(findConfirmDangerButton().exists()).toBe(false);
});
@@ -217,7 +230,7 @@ describe('Settings Panel', () => {
describe('showVisibilityConfirmModal=true', () => {
beforeEach(() => {
wrapper = mountComponent({
- currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER },
showVisibilityConfirmModal: true,
});
});
@@ -225,7 +238,7 @@ describe('Settings Panel', () => {
it('will render the confirmation dialog if the visibility is reduced', async () => {
expect(findConfirmDangerButton().exists()).toBe(false);
- await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER);
expect(findConfirmDangerButton().exists()).toBe(true);
});
@@ -233,7 +246,7 @@ describe('Settings Panel', () => {
it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
- await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER);
await findConfirmDangerButton().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toHaveLength(1);
@@ -253,7 +266,9 @@ describe('Settings Panel', () => {
describe('Repository', () => {
it('should set the repository help text when the visibility level is set to private', () => {
- wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } });
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER },
+ });
expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
'View and edit files in this project.',
@@ -261,7 +276,9 @@ describe('Settings Panel', () => {
});
it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => {
- wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC } });
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER },
+ });
expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
'View and edit files in this project. Non-project members have only read access.',
@@ -345,7 +362,7 @@ describe('Settings Panel', () => {
it('should show the container registry public note if the visibility level is public and the registry is available', () => {
wrapper = mountComponent({
currentSettings: {
- visibilityLevel: visibilityOptions.PUBLIC,
+ visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER,
containerRegistryAccessLevel: featureAccessLevel.EVERYONE,
},
registryAvailable: true,
@@ -360,7 +377,7 @@ describe('Settings Panel', () => {
it('should hide the container registry public note if the visibility level is public but the registry is private', () => {
wrapper = mountComponent({
currentSettings: {
- visibilityLevel: visibilityOptions.PUBLIC,
+ visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER,
containerRegistryAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
},
registryAvailable: true,
@@ -371,7 +388,7 @@ describe('Settings Panel', () => {
it('should hide the container registry public note if the visibility level is private and the registry is available', () => {
wrapper = mountComponent({
- currentSettings: { visibilityLevel: visibilityOptions.PRIVATE },
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER },
registryAvailable: true,
});
@@ -380,7 +397,7 @@ describe('Settings Panel', () => {
it('has label for the toggle', () => {
wrapper = mountComponent({
- currentSettings: { visibilityLevel: visibilityOptions.PUBLIC },
+ currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER },
registryAvailable: true,
});
@@ -569,10 +586,10 @@ describe('Settings Panel', () => {
});
it.each`
- visibilityLevel | output
- ${visibilityOptions.PRIVATE} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]}
- ${visibilityOptions.INTERNAL} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]}
- ${visibilityOptions.PUBLIC} | ${[[30, 'Everyone']]}
+ visibilityLevel | output
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[[30, 'Everyone']]}
`(
'renders correct options when visibilityLevel is $visibilityLevel',
async ({ visibilityLevel, output }) => {
@@ -589,23 +606,23 @@ describe('Settings Panel', () => {
);
it.each`
- initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption
- ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
- ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
+ initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
`(
'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel',
async ({
@@ -635,13 +652,13 @@ describe('Settings Panel', () => {
describe('Pages', () => {
it.each`
- visibilityLevel | pagesAccessControlForced | output
- ${visibilityOptions.PRIVATE} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
- ${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
- ${visibilityOptions.INTERNAL} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
- ${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
- ${visibilityOptions.PUBLIC} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
- ${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
+ visibilityLevel | pagesAccessControlForced | output
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]}
`(
'renders correct options when pagesAccessControlForced is $pagesAccessControlForced and visibilityLevel is $visibilityLevel',
async ({ visibilityLevel, pagesAccessControlForced, output }) => {
@@ -760,13 +777,13 @@ describe('Settings Panel', () => {
it('should reduce Metrics visibility level when visibility is set to private', async () => {
wrapper = mountComponent({
currentSettings: {
- visibilityLevel: visibilityOptions.PUBLIC,
+ visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER,
operationsAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.EVERYONE,
},
});
- await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER);
expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
});
@@ -806,4 +823,78 @@ describe('Settings Panel', () => {
});
});
});
+ describe('Feature Flags', () => {
+ describe('with feature flag', () => {
+ it('should show the feature flags toggle', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ });
+
+ expect(findFeatureFlagsSettings().exists()).toBe(true);
+ });
+ });
+ describe('without feature flag', () => {
+ it('should not show the feature flags toggle', () => {
+ wrapper = mountComponent({});
+
+ expect(findFeatureFlagsSettings().exists()).toBe(false);
+ });
+ });
+ });
+ describe('Releases', () => {
+ describe('with feature flag', () => {
+ it('should show the releases toggle', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ });
+
+ expect(findReleasesSettings().exists()).toBe(true);
+ });
+ });
+ describe('without feature flag', () => {
+ it('should not show the releases toggle', () => {
+ wrapper = mountComponent({});
+
+ expect(findReleasesSettings().exists()).toBe(false);
+ });
+ });
+ });
+ describe('Monitor', () => {
+ const expectedAccessLevel = [
+ [10, 'Only Project Members'],
+ [20, 'Everyone With Access'],
+ ];
+ describe('with feature flag', () => {
+ it('shows Monitor toggle instead of Operations toggle', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ });
+
+ expect(findMonitorSettings().exists()).toBe(true);
+ expect(findOperationsSettings().exists()).toBe(false);
+ expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual(
+ expectedAccessLevel,
+ );
+ });
+ it('when monitorAccessLevel is for project members, it is also for everyone', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS },
+ });
+
+ expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE);
+ });
+ });
+ describe('without feature flag', () => {
+ it('shows Operations toggle instead of Monitor toggle', () => {
+ wrapper = mountComponent({});
+
+ expect(findMonitorSettings().exists()).toBe(false);
+ expect(findOperationsSettings().exists()).toBe(true);
+ expect(
+ findOperationsSettings().findComponent(ProjectFeatureSetting).props('options'),
+ ).toEqual(expectedAccessLevel);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
index 108f816fe01..982c81b9272 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -38,7 +38,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findContent = () => wrapper.find('[data-testid="wiki_page_content"]');
+ const findContent = () => wrapper.find('[data-testid="wiki-page-content"]');
describe('when loading content', () => {
beforeEach(() => {
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 204c48f8de1..b37d2f06191 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -39,7 +39,7 @@ describe('WikiForm', () => {
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
const findContentEditor = () => wrapper.findComponent(ContentEditor);
const findClassicEditor = () => wrapper.findComponent(MarkdownField);
- const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const setFormat = (value) => {
const format = findFormat();
@@ -302,19 +302,15 @@ describe('WikiForm', () => {
});
it.each`
- format | enabled | action
+ format | exists | action
${'markdown'} | ${true} | ${'displays'}
${'rdoc'} | ${false} | ${'hides'}
${'asciidoc'} | ${false} | ${'hides'}
${'org'} | ${false} | ${'hides'}
- `('$action toggle editing mode button when format is $format', async ({ format, enabled }) => {
+ `('$action toggle editing mode button when format is $format', async ({ format, exists }) => {
await setFormat(format);
- expect(findToggleEditingModeButton().exists()).toBe(enabled);
- });
-
- it('displays toggle editing mode button', () => {
- expect(findToggleEditingModeButton().exists()).toBe(true);
+ expect(findToggleEditingModeButton().exists()).toBe(exists);
});
describe('when content editor is not active', () => {
@@ -351,15 +347,8 @@ describe('WikiForm', () => {
});
describe('when content editor is active', () => {
- let mockContentEditor;
-
beforeEach(() => {
createWrapper();
- mockContentEditor = {
- getSerializedContent: jest.fn(),
- setSerializedContent: jest.fn(),
- };
-
findToggleEditingModeButton().vm.$emit('input', 'richText');
});
@@ -368,14 +357,7 @@ describe('WikiForm', () => {
});
describe('when clicking the toggle editing mode button', () => {
- const contentEditorFakeSerializedContent = 'fake content';
-
beforeEach(async () => {
- mockContentEditor.getSerializedContent.mockReturnValueOnce(
- contentEditorFakeSerializedContent,
- );
-
- findContentEditor().vm.$emit('initialized', mockContentEditor);
await findToggleEditingModeButton().vm.$emit('input', 'source');
await nextTick();
});
@@ -387,10 +369,6 @@ describe('WikiForm', () => {
it('displays the classic editor', () => {
expect(findClassicEditor().exists()).toBe(true);
});
-
- it('updates the classic editor content field', () => {
- expect(findContent().element.value).toBe(contentEditorFakeSerializedContent);
- });
});
describe('when content editor is loading', () => {
@@ -480,8 +458,14 @@ describe('WikiForm', () => {
});
describe('when wiki content is updated', () => {
+ const updatedMarkdown = 'hello **world**';
+
beforeEach(() => {
- findContentEditor().vm.$emit('change', { empty: false });
+ findContentEditor().vm.$emit('change', {
+ empty: false,
+ changed: true,
+ markdown: updatedMarkdown,
+ });
});
it('sets before unload warning', () => {
@@ -512,16 +496,8 @@ describe('WikiForm', () => {
});
});
- it('updates content from content editor on form submit', async () => {
- // old value
- expect(findContent().element.value).toBe(' My page content ');
-
- // wait for content editor to load
- await waitForPromises();
-
- await triggerFormSubmit();
-
- expect(findContent().element.value).toBe('hello **world**');
+ it('sets content field to the content editor updated markdown', async () => {
+ expect(findContent().element.value).toBe(updatedMarkdown);
});
});
});
diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js
index 627e004ce3e..5460feb66fe 100644
--- a/spec/frontend/performance_bar/components/add_request_spec.js
+++ b/spec/frontend/performance_bar/components/add_request_spec.js
@@ -51,7 +51,7 @@ describe('add request form', () => {
});
it('emits an event to add the request', () => {
- expect(wrapper.emitted()['add-request']).toBeTruthy();
+ expect(wrapper.emitted()['add-request']).toHaveLength(1);
expect(wrapper.emitted()['add-request'][0]).toEqual([
'http://gitlab.example.com/users/root/calendar.json',
]);
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 2ae36740dfb..437d51e02ba 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -257,7 +257,7 @@ describe('detailedMetric', () => {
});
it('displays request warnings', () => {
- expect(wrapper.find(RequestWarning).exists()).toBe(true);
+ expect(wrapper.findComponent(RequestWarning).exists()).toBe(true);
});
it('can open and close traces', async () => {
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index bff8fcda9b9..9cd5bb9e9a1 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -201,7 +201,7 @@ describe('PersistentUserCallout', () => {
await waitForPromises();
- expect(window.location.assign).toBeCalledWith(href);
+ expect(window.location.assign).toHaveBeenCalledWith(href);
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
index bec6c2a8d0c..0ee6da9d329 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -152,7 +152,7 @@ describe('Pipeline Editor | Commit Form', () => {
});
it('emits "scrolled-to-commit-form"', () => {
- expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy();
+ expect(wrapper.emitted()['scrolled-to-commit-form']).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
index 33c76309951..744b0378a75 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -224,7 +224,7 @@ describe('Pipeline Editor | Commit section', () => {
});
it('emits a commit event with the right type, sourceBranch and targetBranch', () => {
- expect(wrapper.emitted('commit')).toBeTruthy();
+ expect(wrapper.emitted('commit')).toHaveLength(1);
expect(wrapper.emitted('commit')[0]).toMatchObject([
{
type: COMMIT_SUCCESS_WITH_REDIRECT,
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index 7dbacad34bf..8f6f4d8cff9 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -119,7 +119,7 @@ describe('Pipeline editor branch switcher', () => {
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll);
diff --git a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
index 04a93e8db25..f79074f1e0f 100644
--- a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
@@ -31,7 +31,7 @@ describe('Pipeline editor file nav', () => {
const findTip = () => wrapper.findComponent(GlAlert);
const findCurrentConfigFilename = () => wrapper.findByTestId('current-config-filename');
- const fileTreeItems = () => wrapper.findAll(PipelineEditorFileTreeItem);
+ const fileTreeItems = () => wrapper.findAllComponents(PipelineEditorFileTreeItem);
afterEach(() => {
localStorage.clear();
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
new file mode 100644
index 00000000000..d40a9cc8100
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -0,0 +1,109 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('Pipeline Status', () => {
+ let wrapper;
+ let mockApollo;
+ let mockLinkedPipelinesQuery;
+
+ const createComponent = ({ hasStages = true, options } = {}) => {
+ wrapper = shallowMount(PipelineEditorMiniGraph, {
+ provide: {
+ dataMethod: 'graphql',
+ projectFullPath: mockProjectFullPath,
+ },
+ propsData: {
+ pipeline: mockProjectPipeline({ hasStages }).pipeline,
+ },
+ ...options,
+ });
+ };
+
+ const createComponentWithApollo = (hasStages = true) => {
+ const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ hasStages,
+ options: {
+ apolloProvider: mockApollo,
+ },
+ });
+ };
+
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+
+ beforeEach(() => {
+ mockLinkedPipelinesQuery = jest.fn();
+ });
+
+ afterEach(() => {
+ mockLinkedPipelinesQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ describe('when there are stages', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(true);
+ });
+ });
+
+ describe('when there are no stages', () => {
+ beforeEach(() => {
+ createComponent({ hasStages: false });
+ });
+
+ it('does not render pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('when querying upstream and downstream pipelines', () => {
+ describe('when query succeeds', () => {
+ beforeEach(() => {
+ mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
+ createComponentWithApollo();
+ });
+
+ it('should call the query with the correct variables', () => {
+ expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
+ expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
+ fullPath: mockProjectFullPath,
+ iid: mockProjectPipeline().pipeline.iid,
+ });
+ });
+ });
+
+ describe('when query fails', () => {
+ beforeEach(async () => {
+ mockLinkedPipelinesQuery.mockRejectedValue(new Error());
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('should emit an error event when query fails', async () => {
+ expect(wrapper.emitted('showError')).toHaveLength(1);
+ expect(wrapper.emitted('showError')[0]).toEqual([
+ {
+ type: PIPELINE_FAILURE,
+ reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
+ },
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
index 93eb18c90cf..d40a9cc8100 100644
--- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
index 82ac390971d..7f89eda4dff 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -24,11 +24,11 @@ describe('CI Lint Results', () => {
});
};
- const findTable = () => wrapper.find(GlTableLite);
+ const findTable = () => wrapper.findComponent(GlTableLite);
const findByTestId = (selector) => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`);
const findAllByTestId = (selector) => () =>
wrapper.findAll(`[data-testid="ci-lint-${selector}"]`);
- const findLinkToDoc = () => wrapper.find(GlLink);
+ const findLinkToDoc = () => wrapper.findComponent(GlLink);
const findErrors = findByTestId('errors');
const findWarnings = findByTestId('warnings');
const findStatus = findByTestId('status');
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index 4b576508ee9..36052a2e16a 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -17,9 +17,9 @@ describe('CI lint warnings', () => {
});
};
- const findWarningAlert = () => wrapper.find(GlAlert);
+ const findWarningAlert = () => wrapper.findComponent(GlAlert);
const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]');
- const findWarningMessage = () => trimText(wrapper.find(GlSprintf).text());
+ const findWarningMessage = () => trimText(wrapper.findComponent(GlSprintf).text());
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 2f3e1b49b37..3b79739630d 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -256,7 +256,7 @@ describe('Pipeline editor tabs component', () => {
${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false}
${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true}
`(
- 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged ',
+ 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged',
({ appStatus, editor, viz, validate, merged }) => {
createComponent({ appStatus });
diff --git a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index 8d172a8462a..b86c82850c5 100644
--- a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -23,7 +23,7 @@ describe('WalkthroughPopover component', () => {
});
it('emits "walkthrough-popover-cta-clicked" event', async () => {
- expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy();
+ expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
index 3a40ce32a24..24f27e8c5fb 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
@@ -58,7 +58,7 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
const findSlotComponent = () => wrapper.findComponent(MockSourceEditor);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findBadges = () => wrapper.findAll(GlBadge);
+ const findBadges = () => wrapper.findAllComponents(GlBadge);
beforeEach(() => {
mockChildMounted = jest.fn();
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 0ce6cc3f2d4..1989f23a415 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -149,8 +149,7 @@ describe('Pipeline editor app component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
- const findEmptyStateButton = () =>
- wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
+ const findEmptyStateButton = () => findEmptyState().findComponent(GlButton);
const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
beforeEach(() => {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 0cb7155c8c0..e317d1ddcc2 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -254,7 +254,7 @@ describe('Pipeline editor home wrapper', () => {
expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
- findPipelineEditorDrawer().find(GlDrawer).vm.$emit('close');
+ findPipelineEditorDrawer().findComponent(GlDrawer).vm.$emit('close');
await nextTick();
expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
diff --git a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js
new file mode 100644
index 00000000000..512b152f106
--- /dev/null
+++ b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js
@@ -0,0 +1,456 @@
+import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { redirectTo } from '~/lib/utils/url_utility';
+import LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue';
+import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
+import {
+ mockQueryParams,
+ mockPostParams,
+ mockProjectId,
+ mockError,
+ mockRefs,
+ mockCreditCardValidationRequiredError,
+} from '../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+}));
+
+const projectRefsEndpoint = '/root/project/refs';
+const pipelinesPath = '/root/project/-/pipelines';
+const configVariablesPath = '/root/project/-/pipelines/config_variables';
+const newPipelinePostResponse = { id: 1 };
+const defaultBranch = 'main';
+
+describe('Pipeline New Form', () => {
+ let wrapper;
+ let mock;
+ let dummySubmitEvent;
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
+ const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
+ const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
+ const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
+ const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
+ const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
+ const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
+ const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
+ const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
+ const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
+ const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
+ const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
+
+ const selectBranch = (branch) => {
+ // Select a branch in the dropdown
+ findRefsDropdown().vm.$emit('input', {
+ shortName: branch,
+ fullName: `refs/heads/${branch}`,
+ });
+ };
+
+ const createComponent = (props = {}, method = shallowMount) => {
+ wrapper = method(LegacyPipelineNewForm, {
+ provide: {
+ projectRefsEndpoint,
+ },
+ propsData: {
+ projectId: mockProjectId,
+ pipelinesPath,
+ configVariablesPath,
+ defaultBranch,
+ refParam: defaultBranch,
+ settingsLink: '',
+ maxWarnings: 25,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
+ mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
+
+ dummySubmitEvent = {
+ preventDefault: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ mock.restore();
+ });
+
+ describe('Form', () => {
+ beforeEach(async () => {
+ createComponent(mockQueryParams, mount);
+
+ mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
+
+ await waitForPromises();
+ });
+
+ it('displays the correct values for the provided query params', async () => {
+ expect(findDropdowns().at(0).props('text')).toBe('Variable');
+ expect(findDropdowns().at(1).props('text')).toBe('File');
+ expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
+ expect(findVariableRows()).toHaveLength(3);
+ });
+
+ it('displays a variable from provided query params', () => {
+ expect(findKeyInputs().at(0).element.value).toBe('test_var');
+ expect(findValueInputs().at(0).element.value).toBe('test_var_val');
+ });
+
+ it('displays an empty variable for the user to fill out', async () => {
+ expect(findKeyInputs().at(2).element.value).toBe('');
+ expect(findValueInputs().at(2).element.value).toBe('');
+ expect(findDropdowns().at(2).props('text')).toBe('Variable');
+ });
+
+ it('does not display remove icon for last row', () => {
+ expect(findRemoveIcons()).toHaveLength(2);
+ });
+
+ it('removes ci variable row on remove icon button click', async () => {
+ findRemoveIcons().at(1).trigger('click');
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(2);
+ });
+
+ it('creates blank variable on input change event', async () => {
+ const input = findKeyInputs().at(2);
+ input.element.value = 'test_var_2';
+ input.trigger('change');
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(4);
+ expect(findKeyInputs().at(3).element.value).toBe('');
+ expect(findValueInputs().at(3).element.value).toBe('');
+ });
+ });
+
+ describe('Pipeline creation', () => {
+ beforeEach(async () => {
+ mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
+
+ await waitForPromises();
+ });
+
+ it('does not submit the native HTML form', async () => {
+ createComponent();
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ expect(dummySubmitEvent.preventDefault).toHaveBeenCalled();
+ });
+
+ it('disables the submit button immediately after submitting', async () => {
+ createComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+ await waitForPromises();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
+ it('creates pipeline with full ref and variables', async () => {
+ createComponent();
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+ await waitForPromises();
+
+ expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
+ });
+
+ it('creates a pipeline with short ref and variables from the query params', async () => {
+ createComponent(mockQueryParams);
+
+ await waitForPromises();
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ await waitForPromises();
+
+ expect(getFormPostParams()).toEqual(mockPostParams);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
+ });
+ });
+
+ describe('When the ref has been changed', () => {
+ beforeEach(async () => {
+ createComponent({}, mount);
+
+ await waitForPromises();
+ });
+ it('variables persist between ref changes', async () => {
+ selectBranch('main');
+
+ await waitForPromises();
+
+ const mainInput = findKeyInputs().at(0);
+ mainInput.element.value = 'build_var';
+ mainInput.trigger('change');
+
+ await nextTick();
+
+ selectBranch('branch-1');
+
+ await waitForPromises();
+
+ const branchOneInput = findKeyInputs().at(0);
+ branchOneInput.element.value = 'deploy_var';
+ branchOneInput.trigger('change');
+
+ await nextTick();
+
+ selectBranch('main');
+
+ await waitForPromises();
+
+ expect(findKeyInputs().at(0).element.value).toBe('build_var');
+ expect(findVariableRows().length).toBe(2);
+
+ selectBranch('branch-1');
+
+ await waitForPromises();
+
+ expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
+ expect(findVariableRows().length).toBe(2);
+ });
+ });
+
+ describe('when yml defines a variable', () => {
+ const mockYmlKey = 'yml_var';
+ const mockYmlValue = 'yml_var_val';
+ const mockYmlMultiLineValue = `A value
+ with multiple
+ lines`;
+ const mockYmlDesc = 'A var from yml.';
+
+ it('loading icon is shown when content is requested and hidden when received', async () => {
+ createComponent(mockQueryParams, mount);
+
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlValue,
+ description: mockYmlDesc,
+ },
+ });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('multi-line strings are added to the value field without removing line breaks', async () => {
+ createComponent(mockQueryParams, mount);
+
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlMultiLineValue,
+ description: mockYmlDesc,
+ },
+ });
+
+ await waitForPromises();
+
+ expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
+ });
+
+ describe('with description', () => {
+ beforeEach(async () => {
+ createComponent(mockQueryParams, mount);
+
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlValue,
+ description: mockYmlDesc,
+ },
+ });
+
+ await waitForPromises();
+ });
+
+ it('displays all the variables', async () => {
+ expect(findVariableRows()).toHaveLength(4);
+ });
+
+ it('displays a variable from yml', () => {
+ expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
+ expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
+ });
+
+ it('displays a variable from provided query params', () => {
+ expect(findKeyInputs().at(1).element.value).toBe('test_var');
+ expect(findValueInputs().at(1).element.value).toBe('test_var_val');
+ });
+
+ it('adds a description to the first variable from yml', () => {
+ expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
+ });
+
+ it('removes the description when a variable key changes', async () => {
+ findKeyInputs().at(0).element.value = 'yml_var_modified';
+ findKeyInputs().at(0).trigger('change');
+
+ await nextTick();
+
+ expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
+ });
+ });
+
+ describe('without description', () => {
+ beforeEach(async () => {
+ createComponent(mockQueryParams, mount);
+
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlValue,
+ description: null,
+ },
+ yml_var2: {
+ value: 'yml_var2_val',
+ },
+ yml_var3: {
+ description: '',
+ },
+ });
+
+ await waitForPromises();
+ });
+
+ it('displays all the variables', async () => {
+ expect(findVariableRows()).toHaveLength(3);
+ });
+ });
+ });
+
+ describe('Form errors and warnings', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when the refs cannot be loaded', () => {
+ beforeEach(() => {
+ mock
+ .onGet(projectRefsEndpoint, { params: { search: '' } })
+ .reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
+
+ findRefsDropdown().vm.$emit('loadingError');
+ });
+
+ it('shows both an error alert', () => {
+ expect(findErrorAlert().exists()).toBe(true);
+ expect(findWarningAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('when the error response can be handled', () => {
+ beforeEach(async () => {
+ mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ await waitForPromises();
+ });
+
+ it('shows both error and warning', () => {
+ expect(findErrorAlert().exists()).toBe(true);
+ expect(findWarningAlert().exists()).toBe(true);
+ });
+
+ it('shows the correct error', () => {
+ expect(findErrorAlert().text()).toBe(mockError.errors[0]);
+ });
+
+ it('shows the correct warning title', () => {
+ const { length } = mockError.warnings;
+
+ expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ });
+
+ it('shows the correct amount of warnings', () => {
+ expect(findWarnings()).toHaveLength(mockError.warnings.length);
+ });
+
+ it('re-enables the submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+
+ it('does not show the credit card validation required alert', () => {
+ expect(findCCAlert().exists()).toBe(false);
+ });
+
+ describe('when the error response is credit card validation required', () => {
+ beforeEach(async () => {
+ mock
+ .onPost(pipelinesPath)
+ .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError);
+
+ window.gon = {
+ subscriptions_url: TEST_HOST,
+ payment_form_url: TEST_HOST,
+ };
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ await waitForPromises();
+ });
+
+ it('shows credit card validation required alert', () => {
+ expect(findErrorAlert().exists()).toBe(false);
+ expect(findCCAlert().exists()).toBe(true);
+ });
+
+ it('clears error and hides the alert on dismiss', async () => {
+ expect(findCCAlert().exists()).toBe(true);
+ expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]);
+
+ findCCAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findCCAlert().exists()).toBe(false);
+ expect(wrapper.vm.$data.error).toBe(null);
+ });
+ });
+ });
+
+ describe('when the error response cannot be handled', () => {
+ beforeEach(async () => {
+ mock
+ .onPost(pipelinesPath)
+ .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ await waitForPromises();
+ });
+
+ it('re-enables the submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 18dbd1ce9d6..5ce29bd6c5d 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -34,7 +34,7 @@ describe('Pipeline New Form', () => {
let mock;
let dummySubmitEvent;
- const findForm = () => wrapper.find(GlForm);
+ const findForm = () => wrapper.findComponent(GlForm);
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
@@ -44,9 +44,9 @@ describe('Pipeline New Form', () => {
const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
- const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
+ const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
@@ -329,6 +329,12 @@ describe('Pipeline New Form', () => {
value: mockYmlValue,
description: null,
},
+ yml_var2: {
+ value: 'yml_var2_val',
+ },
+ yml_var3: {
+ description: '',
+ },
});
await waitForPromises();
diff --git a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/pipeline_new/components/refs_dropdown_spec.js
index 826f2826d3c..8cba876c688 100644
--- a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js
+++ b/spec/frontend/pipeline_new/components/refs_dropdown_spec.js
@@ -19,8 +19,8 @@ describe('Pipeline New Form', () => {
let wrapper;
let mock;
- const findDropdown = () => wrapper.find(GlDropdown);
- const findRefsDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findRefsDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const createComponent = (props = {}, mountFn = shallowMount) => {
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index c987accbb0d..d7e019c642e 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -174,7 +174,7 @@ describe('Pipeline Wizard - Commit Page', () => {
});
it('will not emit a done event', () => {
- expect(wrapper.emitted().done?.length).toBeFalsy();
+ expect(wrapper.emitted().done?.length).toBeUndefined();
});
afterEach(() => {
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index 540a08d2c7f..26e4b8eb0ea 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -11,7 +11,7 @@ describe('Pages Yaml Editor wrapper', () => {
const wrapper = mount(YamlEditor, defaultOptions);
it('editor is mounted', () => {
- expect(wrapper.vm.editor).not.toBeFalsy();
+ expect(wrapper.vm.editor).not.toBeUndefined();
expect(wrapper.find('.gl-source-editor').exists()).toBe(true);
});
});
@@ -57,13 +57,4 @@ describe('Pages Yaml Editor wrapper', () => {
});
});
});
-
- describe('events', () => {
- const wrapper = mount(YamlEditor, defaultOptions);
-
- it('emits touch if content is changed in editor', async () => {
- await wrapper.vm.editor.setValue('foo: boo');
- expect(wrapper.emitted('touch')).toEqual([expect.any(Array)]);
- });
- });
});
diff --git a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
index ea2448b1362..f288264a11e 100644
--- a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
@@ -30,7 +30,7 @@ describe('Pipeline Wizard -- Input Wrapper', () => {
beforeEach(() => {
createComponent({});
- inputChild = wrapper.find(TextWidget);
+ inputChild = wrapper.findComponent(TextWidget);
});
afterEach(() => {
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index 357a9d21723..f064bf01c86 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -2,6 +2,7 @@ import { Document, parseDocument } from 'yaml';
import { GlProgressBar } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import PipelineWizardWrapper, { i18n } from '~/pipeline_wizard/components/wrapper.vue';
import WizardStep from '~/pipeline_wizard/components/step.vue';
import CommitStep from '~/pipeline_wizard/components/commit.vue';
@@ -19,9 +20,11 @@ describe('Pipeline Wizard - wrapper.vue', () => {
const steps = parseDocument(stepsYaml).toJS();
const getAsYamlNode = (value) => new Document(value).contents;
+ const templateId = 'my-namespace/my-template';
const createComponent = (props = {}, mountFn = shallowMountExtended) => {
wrapper = mountFn(PipelineWizardWrapper, {
propsData: {
+ templateId,
projectPath: '/user/repo',
defaultBranch: 'main',
filename: '.gitlab-ci.yml',
@@ -311,4 +314,126 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
});
});
+
+ describe('when commit step done', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('emits done', () => {
+ expect(wrapper.emitted('done')).toBeUndefined();
+
+ wrapper.findComponent(CommitStep).vm.$emit('done');
+
+ expect(wrapper.emitted('done')).toHaveLength(1);
+ });
+ });
+
+ describe('tracking', () => {
+ let trackingSpy;
+ const trackingCategory = `pipeline_wizard:${templateId}`;
+
+ const setUpTrackingSpy = () => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ };
+
+ it('tracks next button click event', () => {
+ createComponent();
+ setUpTrackingSpy();
+ findFirstVisibleStep().vm.$emit('next');
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', {
+ category: trackingCategory,
+ property: 'next',
+ label: 'pipeline_wizard_navigation',
+ extra: {
+ fromStep: 0,
+ toStep: 1,
+ },
+ });
+ });
+
+ it('tracks back button click event', () => {
+ createComponent();
+
+ // Navigate to step 1 without the spy set up
+ findFirstVisibleStep().vm.$emit('next');
+
+ // Now enable the tracking spy
+ setUpTrackingSpy();
+
+ findFirstVisibleStep().vm.$emit('back');
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', {
+ category: trackingCategory,
+ property: 'back',
+ label: 'pipeline_wizard_navigation',
+ extra: {
+ fromStep: 1,
+ toStep: 0,
+ },
+ });
+ });
+
+ it('tracks back button click event on the commit step', () => {
+ createComponent();
+
+ // Navigate to step 2 without the spy set up
+ findFirstVisibleStep().vm.$emit('next');
+ findFirstVisibleStep().vm.$emit('next');
+
+ // Now enable the tracking spy
+ setUpTrackingSpy();
+
+ wrapper.findComponent(CommitStep).vm.$emit('back');
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', {
+ category: trackingCategory,
+ property: 'back',
+ label: 'pipeline_wizard_navigation',
+ extra: {
+ fromStep: 2,
+ toStep: 1,
+ },
+ });
+ });
+
+ it('tracks done event on the commit step', () => {
+ createComponent();
+
+ // Navigate to step 2 without the spy set up
+ findFirstVisibleStep().vm.$emit('next');
+ findFirstVisibleStep().vm.$emit('next');
+
+ // Now enable the tracking spy
+ setUpTrackingSpy();
+
+ wrapper.findComponent(CommitStep).vm.$emit('done');
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', {
+ category: trackingCategory,
+ label: 'pipeline_wizard_commit',
+ property: 'commit',
+ });
+ });
+
+ it('tracks when editor emits touch events', () => {
+ createComponent();
+ setUpTrackingSpy();
+
+ wrapper.findComponent(YamlEditor).vm.$emit('touch');
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'edit', {
+ category: trackingCategory,
+ label: 'pipeline_wizard_editor_interaction',
+ extra: {
+ currentStep: 0,
+ },
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js
index e7087b59ce7..12b6f1052b2 100644
--- a/spec/frontend/pipeline_wizard/mock/yaml.js
+++ b/spec/frontend/pipeline_wizard/mock/yaml.js
@@ -71,6 +71,7 @@ bar: barVal
`;
export const fullTemplate = `
+id: test/full-template
title: some title
description: some description
filename: foo.yml
@@ -84,6 +85,7 @@ steps:
`;
export const fullTemplateWithoutFilename = `
+id: test/full-template-no-filename
title: some title
description: some description
steps:
diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
index 3f689ffdbc8..13234525159 100644
--- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
+++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
@@ -59,6 +59,7 @@ describe('PipelineWizard', () => {
defaultBranch,
projectPath,
filename: parseDocument(template).get('filename'),
+ templateId: parseDocument(template).get('id'),
}),
);
});
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
index 212f8e19a6d..28a08b6da0f 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
@@ -11,7 +11,7 @@ describe('The DAG annotations', () => {
const getAllColorBlocks = () => wrapper.findAll('[data-testid="dag-color-block"]');
const getTextBlock = () => wrapper.find('[data-testid="dag-note-text"]');
const getAllTextBlocks = () => wrapper.findAll('[data-testid="dag-note-text"]');
- const getToggleButton = () => wrapper.find(GlButton);
+ const getToggleButton = () => wrapper.findComponent(GlButton);
const createComponent = (propsData = {}, method = shallowMount) => {
if (wrapper?.destroy) {
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index d78df3eb35e..b0c26976c85 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -18,12 +18,12 @@ import {
describe('Pipeline DAG graph wrapper', () => {
let wrapper;
- const getAlert = () => wrapper.find(GlAlert);
- const getAllAlerts = () => wrapper.findAll(GlAlert);
- const getGraph = () => wrapper.find(DagGraph);
- const getNotes = () => wrapper.find(DagAnnotations);
+ const getAlert = () => wrapper.findComponent(GlAlert);
+ const getAllAlerts = () => wrapper.findAllComponents(GlAlert);
+ const getGraph = () => wrapper.findComponent(DagGraph);
+ const getNotes = () => wrapper.findComponent(DagAnnotations);
const getErrorText = (type) => wrapper.vm.$options.errorTexts[type];
- const getEmptyState = () => wrapper.find(GlEmptyState);
+ const getEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({
graphData = mockParsedGraphQLNodes,
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
new file mode 100644
index 00000000000..5ea57c51e70
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -0,0 +1,176 @@
+import { mount } from '@vue/test-utils';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import LinkedPipelinesMiniList from '~/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue';
+import mockData from './linked_pipelines_mock_data';
+
+describe('Linked pipeline mini list', () => {
+ let wrapper;
+
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
+ const findCiIcons = () => wrapper.findAllComponents(CiIcon);
+ const findLinkedPipelineCounter = () => wrapper.find('[data-testid="linked-pipeline-counter"]');
+ const findLinkedPipelineMiniItem = () =>
+ wrapper.find('[data-testid="linked-pipeline-mini-item"]');
+ const findLinkedPipelineMiniItems = () =>
+ wrapper.findAll('[data-testid="linked-pipeline-mini-item"]');
+ const findLinkedPipelineMiniList = () => wrapper.findComponent(LinkedPipelinesMiniList);
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(LinkedPipelinesMiniList, {
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('when passed an upstream pipeline as prop', () => {
+ beforeEach(() => {
+ createComponent({
+ triggeredBy: [mockData.triggered_by],
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render one linked pipeline item', () => {
+ expect(findLinkedPipelineMiniItem().exists()).toBe(true);
+ });
+
+ it('should render a linked pipeline with the correct href', () => {
+ expect(findLinkedPipelineMiniItem().exists()).toBe(true);
+
+ expect(findLinkedPipelineMiniItem().attributes('href')).toBe(
+ '/gitlab-org/gitlab-foss/-/pipelines/129',
+ );
+ });
+
+ it('should render one ci status icon', () => {
+ expect(findCiIcon().exists()).toBe(true);
+ });
+
+ it('should render a borderless ci-icon', () => {
+ expect(findCiIcon().exists()).toBe(true);
+
+ expect(findCiIcon().props('isBorderless')).toBe(true);
+ expect(findCiIcon().classes('borderless')).toBe(true);
+ });
+
+ it('should render a ci-icon with a custom border class', () => {
+ expect(findCiIcon().exists()).toBe(true);
+
+ expect(findCiIcon().classes('gl-border')).toBe(true);
+ });
+
+ it('should render the correct ci status icon', () => {
+ expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
+ });
+
+ it('should have an activated tooltip', () => {
+ expect(findLinkedPipelineMiniItem().exists()).toBe(true);
+ const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe('GitLabCE - running');
+ });
+
+ it('should correctly set is-upstream', () => {
+ expect(findLinkedPipelineMiniList().exists()).toBe(true);
+
+ expect(findLinkedPipelineMiniList().classes('is-upstream')).toBe(true);
+ });
+
+ it('should correctly compute shouldRenderCounter', () => {
+ expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(false);
+ });
+
+ it('should not render the pipeline counter', () => {
+ expect(findLinkedPipelineCounter().exists()).toBe(false);
+ });
+ });
+
+ describe('when passed downstream pipelines as props', () => {
+ beforeEach(() => {
+ createComponent({
+ triggered: mockData.triggered,
+ pipelinePath: 'my/pipeline/path',
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render three linked pipeline items', () => {
+ expect(findLinkedPipelineMiniItems().exists()).toBe(true);
+ expect(findLinkedPipelineMiniItems().length).toBe(3);
+ });
+
+ it('should render three ci status icons', () => {
+ expect(findCiIcons().exists()).toBe(true);
+ expect(findCiIcons().length).toBe(3);
+ });
+
+ it('should render the correct ci status icon', () => {
+ expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
+ });
+
+ it('should have an activated tooltip', () => {
+ expect(findLinkedPipelineMiniItem().exists()).toBe(true);
+ const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe('GitLabCE - running');
+ });
+
+ it('should correctly set is-downstream', () => {
+ expect(findLinkedPipelineMiniList().exists()).toBe(true);
+
+ expect(findLinkedPipelineMiniList().classes('is-downstream')).toBe(true);
+ });
+
+ it('should render a borderless ci-icon', () => {
+ expect(findCiIcon().exists()).toBe(true);
+
+ expect(findCiIcon().props('isBorderless')).toBe(true);
+ expect(findCiIcon().classes('borderless')).toBe(true);
+ });
+
+ it('should render a ci-icon with a custom border class', () => {
+ expect(findCiIcon().exists()).toBe(true);
+
+ expect(findCiIcon().classes('gl-border')).toBe(true);
+ });
+
+ it('should render the pipeline counter', () => {
+ expect(findLinkedPipelineCounter().exists()).toBe(true);
+ });
+
+ it('should correctly compute shouldRenderCounter', () => {
+ expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(true);
+ });
+
+ it('should correctly trim linkedPipelines', () => {
+ expect(findLinkedPipelineMiniList().props('triggered').length).toBe(6);
+ expect(findLinkedPipelineMiniList().vm.linkedPipelinesTrimmed.length).toBe(3);
+ });
+
+ it('should set the correct pipeline path', () => {
+ expect(findLinkedPipelineCounter().exists()).toBe(true);
+
+ expect(findLinkedPipelineCounter().attributes('href')).toBe('my/pipeline/path');
+ });
+
+ it('should render the correct counterTooltipText', () => {
+ expect(findLinkedPipelineCounter().exists()).toBe(true);
+ const tooltip = getBinding(findLinkedPipelineCounter().element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe(findLinkedPipelineMiniList().vm.counterTooltipText);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js
new file mode 100644
index 00000000000..117c7f2ae52
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js
@@ -0,0 +1,407 @@
+export default {
+ triggered_by: {
+ id: 129,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/129',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/129',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: '7-5-stable',
+ path: '/gitlab-org/gitlab-foss/commits/7-5-stable',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '23433d4d8b20d7e45c103d0b6048faad38a130ab',
+ short_id: '23433d4d',
+ title: 'Version 7.5.0.rc1',
+ created_at: '2014-11-17T15:44:14.000+01:00',
+ parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'],
+ message: 'Version 7.5.0.rc1\n',
+ author_name: 'Jacob Vosmaer',
+ author_email: 'contact@jacobvosmaer.nl',
+ authored_date: '2014-11-17T15:44:14.000+01:00',
+ committer_name: 'Jacob Vosmaer',
+ committer_email: 'contact@jacobvosmaer.nl',
+ committed_date: '2014-11-17T15:44:14.000+01:00',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab',
+ commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/129/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/129/cancel',
+ created_at: '2017-05-24T14:46:20.090Z',
+ updated_at: '2017-05-24T14:46:29.906Z',
+ },
+ triggered: [
+ {
+ id: 132,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/132',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/132',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ short_id: 'b9d58c4c',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-03T12:50:33.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-03T12:50:33.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel',
+ created_at: '2017-05-24T14:46:24.644Z',
+ updated_at: '2017-05-24T14:48:55.226Z',
+ },
+ {
+ id: 133,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/133',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/133',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ short_id: 'b6bd4856',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-02T20:39:29.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-02T20:39:29.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel',
+ created_at: '2017-05-24T14:46:24.648Z',
+ updated_at: '2017-05-24T14:48:59.673Z',
+ },
+ {
+ id: 130,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/130',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/130',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ short_id: '6d7ced4a',
+ title: 'Whitespace fixes to patch',
+ created_at: '2013-10-08T13:53:22.000-05:00',
+ parent_ids: ['1875141a963a4238bda29011d8f7105839485253'],
+ message: 'Whitespace fixes to patch\n',
+ author_name: 'Dale Hamel',
+ author_email: 'dale.hamel@srvthe.net',
+ authored_date: '2013-10-08T13:53:22.000-05:00',
+ committer_name: 'Dale Hamel',
+ committer_email: 'dale.hamel@invenia.ca',
+ committed_date: '2013-10-08T13:53:22.000-05:00',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel',
+ created_at: '2017-05-24T14:46:24.630Z',
+ updated_at: '2017-05-24T14:49:45.091Z',
+ },
+ {
+ id: 131,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/132',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/132',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ short_id: 'b9d58c4c',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-03T12:50:33.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-03T12:50:33.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel',
+ created_at: '2017-05-24T14:46:24.644Z',
+ updated_at: '2017-05-24T14:48:55.226Z',
+ },
+ {
+ id: 134,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/133',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/133',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ short_id: 'b6bd4856',
+ title: 'getting user keys publically through http without any authentication, the github…',
+ created_at: '2013-10-02T20:39:29.000+05:30',
+ parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
+ message:
+ 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n',
+ author_name: 'devaroop',
+ author_email: 'devaroop123@yahoo.co.in',
+ authored_date: '2013-10-02T20:39:29.000+05:30',
+ committer_name: 'devaroop',
+ committer_email: 'devaroop123@yahoo.co.in',
+ committed_date: '2013-10-02T20:39:29.000+05:30',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel',
+ created_at: '2017-05-24T14:46:24.648Z',
+ updated_at: '2017-05-24T14:48:59.673Z',
+ },
+ {
+ id: 135,
+ active: true,
+ path: '/gitlab-org/gitlab-foss/-/pipelines/130',
+ project: {
+ name: 'GitLabCE',
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-foss/-/pipelines/130',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ },
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: true,
+ },
+ ref: {
+ name: 'crowd',
+ path: '/gitlab-org/gitlab-foss/commits/crowd',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ short_id: '6d7ced4a',
+ title: 'Whitespace fixes to patch',
+ created_at: '2013-10-08T13:53:22.000-05:00',
+ parent_ids: ['1875141a963a4238bda29011d8f7105839485253'],
+ message: 'Whitespace fixes to patch\n',
+ author_name: 'Dale Hamel',
+ author_email: 'dale.hamel@srvthe.net',
+ authored_date: '2013-10-08T13:53:22.000-05:00',
+ committer_name: 'Dale Hamel',
+ committer_email: 'dale.hamel@invenia.ca',
+ committed_date: '2013-10-08T13:53:22.000-05:00',
+ author_gravatar_url:
+ 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
+ },
+ retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry',
+ cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel',
+ created_at: '2017-05-24T14:46:24.630Z',
+ updated_at: '2017-05-24T14:49:45.091Z',
+ },
+ ],
+};
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
new file mode 100644
index 00000000000..7fa8a18ea1f
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
@@ -0,0 +1,149 @@
+import { mount } from '@vue/test-utils';
+import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
+import mockLinkedPipelines from './linked_pipelines_mock_data';
+
+const mockStages = pipelines[0].details.stages;
+
+describe('Pipeline Mini Graph', () => {
+ let wrapper;
+
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+ const findPipelineStages = () => wrapper.findComponent(PipelineStages);
+
+ const findLinkedPipelineUpstream = () =>
+ wrapper.findComponent('[data-testid="pipeline-mini-graph-upstream"]');
+ const findLinkedPipelineDownstream = () =>
+ wrapper.findComponent('[data-testid="pipeline-mini-graph-downstream"]');
+ const findDownstreamArrowIcon = () => wrapper.find('[data-testid="downstream-arrow-icon"]');
+ const findUpstreamArrowIcon = () => wrapper.find('[data-testid="upstream-arrow-icon"]');
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(PipelineMiniGraph, {
+ propsData: {
+ stages: mockStages,
+ ...props,
+ },
+ });
+ };
+
+ describe('rendered state without upstream or downstream pipelines', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render the pipeline stages', () => {
+ expect(findPipelineStages().exists()).toBe(true);
+ });
+
+ it('should have the correct props', () => {
+ expect(findPipelineMiniGraph().props()).toMatchObject({
+ downstreamPipelines: [],
+ isMergeTrain: false,
+ pipelinePath: '',
+ stages: expect.any(Array),
+ stagesClass: '',
+ updateDropdown: false,
+ upstreamPipeline: undefined,
+ });
+ });
+
+ it('should have no linked pipelines', () => {
+ expect(findLinkedPipelineDownstream().exists()).toBe(false);
+ expect(findLinkedPipelineUpstream().exists()).toBe(false);
+ });
+
+ it('should not render arrow icons', () => {
+ expect(findUpstreamArrowIcon().exists()).toBe(false);
+ expect(findDownstreamArrowIcon().exists()).toBe(false);
+ });
+
+ it('triggers events in "action request complete"', () => {
+ createComponent();
+
+ findPipelineMiniGraph(0).vm.$emit('pipelineActionRequestComplete');
+ findPipelineMiniGraph(1).vm.$emit('pipelineActionRequestComplete');
+
+ expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
+ });
+ });
+
+ describe('rendered state with upstream pipeline', () => {
+ beforeEach(() => {
+ createComponent({
+ upstreamPipeline: mockLinkedPipelines.triggered_by,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should have the correct props', () => {
+ expect(findPipelineMiniGraph().props()).toMatchObject({
+ downstreamPipelines: [],
+ isMergeTrain: false,
+ pipelinePath: '',
+ stages: expect.any(Array),
+ stagesClass: '',
+ updateDropdown: false,
+ upstreamPipeline: expect.any(Object),
+ });
+ });
+
+ it('should render the upstream linked pipelines mini list only', () => {
+ expect(findLinkedPipelineUpstream().exists()).toBe(true);
+ expect(findLinkedPipelineDownstream().exists()).toBe(false);
+ });
+
+ it('should render an upstream arrow icon only', () => {
+ expect(findDownstreamArrowIcon().exists()).toBe(false);
+ expect(findUpstreamArrowIcon().exists()).toBe(true);
+ expect(findUpstreamArrowIcon().props('name')).toBe('long-arrow');
+ });
+ });
+
+ describe('rendered state with downstream pipelines', () => {
+ beforeEach(() => {
+ createComponent({
+ downstreamPipelines: mockLinkedPipelines.triggered,
+ pipelinePath: 'my/pipeline/path',
+ });
+ });
+
+ it('should have the correct props', () => {
+ expect(findPipelineMiniGraph().props()).toMatchObject({
+ downstreamPipelines: expect.any(Array),
+ isMergeTrain: false,
+ pipelinePath: 'my/pipeline/path',
+ stages: expect.any(Array),
+ stagesClass: '',
+ updateDropdown: false,
+ upstreamPipeline: undefined,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render the downstream linked pipelines mini list only', () => {
+ expect(findLinkedPipelineDownstream().exists()).toBe(true);
+ expect(findLinkedPipelineUpstream().exists()).toBe(false);
+ });
+
+ it('should render a downstream arrow icon only', () => {
+ expect(findUpstreamArrowIcon().exists()).toBe(false);
+ expect(findDownstreamArrowIcon().exists()).toBe(true);
+ expect(findDownstreamArrowIcon().props('name')).toBe('long-arrow');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index e712cdeaea2..52b440f18bb 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -4,7 +4,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
-import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
+import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
import { stageReply } from '../../mock_data';
@@ -129,10 +129,11 @@ describe('Pipelines stage component', () => {
await axios.waitForAll();
});
- it('renders the received data and emit `clickedDropdown` event', async () => {
+ it('renders the received data and emits the correct events', async () => {
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ expect(wrapper.emitted('miniGraphStageClick')).toEqual([[]]);
});
it('refreshes when updateDropdown is set to true', async () => {
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index 1cb43c199aa..bfb780d5d39 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -1,18 +1,18 @@
import { shallowMount } from '@vue/test-utils';
import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
-import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
-import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
+import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
+import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
const mockStages = pipelines[0].details.stages;
-describe('Pipeline Mini Graph', () => {
+describe('Pipeline Stages', () => {
let wrapper;
- const findPipelineStages = () => wrapper.findAll(PipelineStage);
+ const findPipelineStages = () => wrapper.findAllComponents(PipelineStage);
const findPipelineStagesAt = (i) => findPipelineStages().at(i);
const createComponent = (props = {}) => {
- wrapper = shallowMount(PipelineMiniGraph, {
+ wrapper = shallowMount(PipelineStages, {
propsData: {
stages: mockStages,
...props,
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index f958f12acd4..ee3eaaf5ef3 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -2,17 +2,19 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { TRACKING_CATEGORIES } from '~/pipelines/constants';
import { users, mockSearch, branches, tags } from '../mock_data';
describe('Pipelines filtered search', () => {
let wrapper;
let mock;
- const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const getSearchToken = (type) =>
findFilteredSearch()
.props('availableTokens')
@@ -177,4 +179,20 @@ describe('Pipelines filtered search', () => {
expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length);
});
});
+
+ describe('tracking', () => {
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks filtered search click', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findFilteredSearch().vm.$emit('submit', mockSearch);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filtered_search', {
+ label: TRACKING_CATEGORIES.search,
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 6e5aa572ec0..a823e029281 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -9,7 +9,7 @@ import ActionComponent from '~/pipelines/components/jobs_shared/action_component
describe('pipeline graph action component', () => {
let wrapper;
let mock;
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]');
beforeEach(() => {
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 4b2b61c8edd..2abb5f7dc58 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -15,9 +15,9 @@ import {
describe('graph component', () => {
let wrapper;
- const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn);
- const findLinksLayer = () => wrapper.find(LinksLayer);
- const findStageColumns = () => wrapper.findAll(StageColumnComponent);
+ const findLinkedColumns = () => wrapper.findAllComponents(LinkedPipelinesColumn);
+ const findLinksLayer = () => wrapper.findComponent(LinksLayer);
+ const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
const findStageNameInJob = () => wrapper.find('[data-testid="stage-name-in-job"]');
const defaultProps = {
@@ -107,7 +107,7 @@ describe('graph component', () => {
});
it('dims unrelated jobs', () => {
- const unrelatedJob = wrapper.find(JobItem);
+ const unrelatedJob = wrapper.findComponent(JobItem);
expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1);
expect(unrelatedJob.classes('gl-opacity-3')).toBe(true);
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 3eaf06e0656..587a3c67168 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -30,16 +30,10 @@ import * as Api from '~/pipelines/components/graph_shared/api';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
-import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
-import {
- mapCallouts,
- mockCalloutsResponse,
- mockPipelineResponse,
- mockPerformanceInsightsResponse,
-} from './mock_data';
+import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@@ -57,11 +51,11 @@ describe('Pipeline graph wrapper', () => {
const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const getLinksLayer = () => wrapper.findComponent(LinksLayer);
- const getGraph = () => wrapper.find(PipelineGraph);
+ const getGraph = () => wrapper.findComponent(PipelineGraph);
const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const getAllStageColumnGroupsInColumn = () =>
- wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
- const getViewSelector = () => wrapper.find(GraphViewSelector);
+ wrapper.findComponent(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
+ const getViewSelector = () => wrapper.findComponent(GraphViewSelector);
const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
@@ -95,15 +89,11 @@ describe('Pipeline graph wrapper', () => {
const callouts = mapCallouts(calloutsList);
const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts));
const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData);
- const getPerformanceInsightsHandler = jest
- .fn()
- .mockResolvedValue(mockPerformanceInsightsResponse);
const requestHandlers = [
[getPipelineHeaderData, getPipelineHeaderDataHandler],
[getPipelineDetails, getPipelineDetailsHandler],
[getUserCallouts, getUserCalloutsHandler],
- [getPerformanceInsights, getPerformanceInsightsHandler],
];
const apolloProvider = createMockApollo(requestHandlers);
@@ -309,7 +299,7 @@ describe('Pipeline graph wrapper', () => {
const groupsInFirstColumn =
mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes.length;
expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
- expect(getStageColumnTitle().text()).toBe('Build');
+ expect(getStageColumnTitle().text()).toBe('build');
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
expect(getStageColumnTitle().text()).toBe('');
@@ -418,7 +408,7 @@ describe('Pipeline graph wrapper', () => {
it('reads the view type from localStorage when available', () => {
const viewSelectorNeedsSegment = wrapper
- .find(GlButtonGroup)
+ .findComponent(GlButtonGroup)
.findAllComponents(GlButton)
.at(1);
expect(viewSelectorNeedsSegment.classes()).toContain('selected');
@@ -564,7 +554,7 @@ describe('Pipeline graph wrapper', () => {
mock.restore();
});
- it('it calls reportPerformance with expected arguments', () => {
+ it('calls reportPerformance with expected arguments', () => {
expect(markAndMeasure).toHaveBeenCalled();
expect(reportPerformance).toHaveBeenCalled();
expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 1397500bdc7..43587bebedf 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -1,34 +1,23 @@
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
-import { mockPerformanceInsightsResponse } from './mock_data';
-
-Vue.use(VueApollo);
describe('the graph view selector component', () => {
let wrapper;
- let trackingSpy;
const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const findViewTypeSelector = () => wrapper.findComponent(GlButtonGroup);
const findStageViewButton = () => findViewTypeSelector().findAllComponents(GlButton).at(0);
const findLayerViewButton = () => findViewTypeSelector().findAllComponents(GlButton).at(1);
const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
- const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
+ const findToggleLoader = () => findDependenciesToggle().findComponent(GlLoadingIcon);
const findHoverTip = () => wrapper.findComponent(GlAlert);
- const findPipelineInsightsBtn = () => wrapper.find('[data-testid="pipeline-insights-btn"]');
const defaultProps = {
showLinks: false,
tipPreviouslyDismissed: false,
type: STAGE_VIEW,
- isPipelineComplete: true,
};
const defaultData = {
@@ -38,14 +27,6 @@ describe('the graph view selector component', () => {
showLinksActive: false,
};
- const getPerformanceInsightsHandler = jest
- .fn()
- .mockResolvedValue(mockPerformanceInsightsResponse);
-
- const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]];
-
- const apolloProvider = createMockApollo(requestHandlers);
-
const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(GraphViewSelector, {
propsData: {
@@ -58,7 +39,6 @@ describe('the graph view selector component', () => {
...data,
};
},
- apolloProvider,
});
};
@@ -222,44 +202,5 @@ describe('the graph view selector component', () => {
expect(findHoverTip().exists()).toBe(false);
});
});
-
- describe('pipeline insights', () => {
- it.each`
- isPipelineComplete | shouldShow
- ${true} | ${true}
- ${false} | ${false}
- `(
- 'button should display $shouldShow if isPipelineComplete is $isPipelineComplete ',
- ({ isPipelineComplete, shouldShow }) => {
- createComponent({
- props: {
- isPipelineComplete,
- },
- });
-
- expect(findPipelineInsightsBtn().exists()).toBe(shouldShow);
- },
- );
- });
-
- describe('tracking', () => {
- beforeEach(() => {
- createComponent();
-
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- afterEach(() => {
- unmockTracking();
- });
-
- it('tracks performance insights button click', () => {
- findPipelineInsightsBtn().vm.$emit('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_insights_button', {
- label: 'performance_insights',
- });
- });
- });
});
});
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 4f0da09fec6..05776ec0706 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -59,7 +59,7 @@ describe('pipeline graph job item', () => {
});
});
- it('it should render status and name', () => {
+ it('should render status and name', () => {
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
expect(wrapper.find('a').exists()).toBe(false);
@@ -72,7 +72,7 @@ describe('pipeline graph job item', () => {
});
describe('action icon', () => {
- it('it should render the action icon', () => {
+ it('should render the action icon', () => {
createWrapper({ job: mockJob });
const actionComponent = findActionComponent();
@@ -82,7 +82,7 @@ describe('pipeline graph job item', () => {
expect(actionComponent.attributes('disabled')).not.toBe('disabled');
});
- it('it should render disabled action icon when user cannot run the action', () => {
+ it('should render disabled action icon when user cannot run the action', () => {
createWrapper({ job: mockJobWithUnauthorizedAction });
const actionComponent = findActionComponent();
diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js
index d3008c046e8..ec432e98fdf 100644
--- a/spec/frontend/pipelines/graph/job_name_component_spec.js
+++ b/spec/frontend/pipelines/graph/job_name_component_spec.js
@@ -24,7 +24,7 @@ describe('job name component', () => {
});
it('should render an icon with the provided status', () => {
- expect(wrapper.find(ciIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(ciIcon).exists()).toBe(true);
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 7d1e4774a24..399d52c3dff 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -36,13 +36,13 @@ describe('Linked pipeline', () => {
type: UPSTREAM,
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
const findCardTooltip = () => wrapper.findComponent(GlTooltip);
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
- const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' });
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
@@ -80,7 +80,7 @@ describe('Linked pipeline', () => {
});
it('should render an svg within the status container', () => {
- const pipelineStatusElement = wrapper.find(CiStatus);
+ const pipelineStatusElement = wrapper.findComponent(CiStatus);
expect(pipelineStatusElement.find('svg').exists()).toBe(true);
});
@@ -90,7 +90,7 @@ describe('Linked pipeline', () => {
});
it('should have a ci-status child component', () => {
- expect(wrapper.find(CiStatus).exists()).toBe(true);
+ expect(wrapper.findComponent(CiStatus).exists()).toBe(true);
});
it('should render the pipeline id', () => {
@@ -214,7 +214,7 @@ describe('Linked pipeline', () => {
await findRetryButton().trigger('click');
});
- it('calls the retry mutation ', () => {
+ it('calls the retry mutation', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: RetryPipelineMutation,
@@ -255,7 +255,7 @@ describe('Linked pipeline', () => {
createWrapper({ propsData: cancelablePipeline });
});
- it('shows only the cancel button ', () => {
+ it('shows only the cancel button', () => {
expect(findCancelButton().exists()).toBe(true);
expect(findRetryButton().exists()).toBe(false);
});
@@ -375,7 +375,7 @@ describe('Linked pipeline', () => {
${'mouseover'} | ${'mouseout'}
${'focus'} | ${'blur'}
`(
- 'applies the class on $activateEventName and removes it on $deactivateEventName ',
+ 'applies the class on $activateEventName and removes it on $deactivateEventName',
async ({ activateEventName, deactivateEventName }) => {
const shadowClass = 'gl-shadow-none!';
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 46000711110..63e2d8707ea 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -38,8 +38,8 @@ describe('Linked Pipelines Column', () => {
let wrapper;
const findLinkedColumnTitle = () => wrapper.find('[data-testid="linked-column-title"]');
- const findLinkedPipelineElements = () => wrapper.findAll(LinkedPipeline);
- const findPipelineGraph = () => wrapper.find(PipelineGraph);
+ const findLinkedPipelineElements = () => wrapper.findAllComponents(LinkedPipeline);
+ const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 959bbcefc98..6124d67af09 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1038,245 +1038,3 @@ export const triggerJob = {
action: null,
},
};
-
-export const mockPerformanceInsightsResponse = {
- data: {
- project: {
- __typename: 'Project',
- id: 'gid://gitlab/Project/20',
- pipeline: {
- __typename: 'Pipeline',
- id: 'gid://gitlab/Ci::Pipeline/97',
- jobs: {
- __typename: 'CiJobConnection',
- pageInfo: {
- __typename: 'PageInfo',
- hasNextPage: false,
- },
- nodes: [
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Bridge/2502',
- duration: null,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2502-2502',
- detailsPath: '/root/lots-of-jobs-project/-/pipelines/98',
- },
- name: 'trigger_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/303',
- name: 'deploy',
- },
- startedAt: null,
- queuedDuration: 424850.376278,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2501',
- duration: 10,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2501-2501',
- detailsPath: '/root/ci-project/-/jobs/2501',
- },
- name: 'artifact_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/303',
- name: 'deploy',
- },
- startedAt: '2022-07-01T16:31:41Z',
- queuedDuration: 2.621553,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2500',
- duration: 4,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2500-2500',
- detailsPath: '/root/ci-project/-/jobs/2500',
- },
- name: 'coverage_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/302',
- name: 'test',
- },
- startedAt: '2022-07-01T16:31:33Z',
- queuedDuration: 14.388869,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2499',
- duration: 4,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2499-2499',
- detailsPath: '/root/ci-project/-/jobs/2499',
- },
- name: 'test_job_two',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/302',
- name: 'test',
- },
- startedAt: '2022-07-01T16:31:28Z',
- queuedDuration: 15.792664,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2498',
- duration: 4,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2498-2498',
- detailsPath: '/root/ci-project/-/jobs/2498',
- },
- name: 'test_job_one',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/302',
- name: 'test',
- },
- startedAt: '2022-07-01T16:31:17Z',
- queuedDuration: 8.317072,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2497',
- duration: 5,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'failed-2497-2497',
- detailsPath: '/root/ci-project/-/jobs/2497',
- },
- name: 'allow_failure_test_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/302',
- name: 'test',
- },
- startedAt: '2022-07-01T16:31:22Z',
- queuedDuration: 3.547553,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2496',
- duration: null,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'manual-2496-2496',
- detailsPath: '/root/ci-project/-/jobs/2496',
- },
- name: 'test_manual_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/302',
- name: 'test',
- },
- startedAt: null,
- queuedDuration: null,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2495',
- duration: 5,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2495-2495',
- detailsPath: '/root/ci-project/-/jobs/2495',
- },
- name: 'large_log_output',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/301',
- name: 'build',
- },
- startedAt: '2022-07-01T16:31:11Z',
- queuedDuration: 79.128625,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2494',
- duration: 5,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2494-2494',
- detailsPath: '/root/ci-project/-/jobs/2494',
- },
- name: 'build_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/301',
- name: 'build',
- },
- startedAt: '2022-07-01T16:31:05Z',
- queuedDuration: 73.286895,
- },
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Build/2493',
- duration: 16,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2493-2493',
- detailsPath: '/root/ci-project/-/jobs/2493',
- },
- name: 'wait_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/301',
- name: 'build',
- },
- startedAt: '2022-07-01T16:30:48Z',
- queuedDuration: 56.258856,
- },
- ],
- },
- },
- },
- },
-};
-
-export const mockPerformanceInsightsNextPageResponse = {
- data: {
- project: {
- __typename: 'Project',
- id: 'gid://gitlab/Project/20',
- pipeline: {
- __typename: 'Pipeline',
- id: 'gid://gitlab/Ci::Pipeline/97',
- jobs: {
- __typename: 'CiJobConnection',
- pageInfo: {
- __typename: 'PageInfo',
- hasNextPage: true,
- },
- nodes: [
- {
- __typename: 'CiJob',
- id: 'gid://gitlab/Ci::Bridge/2502',
- duration: null,
- detailedStatus: {
- __typename: 'DetailedStatus',
- id: 'success-2502-2502',
- detailsPath: '/root/lots-of-jobs-project/-/pipelines/98',
- },
- name: 'trigger_job',
- stage: {
- __typename: 'CiStage',
- id: 'gid://gitlab/Ci::Stage/303',
- name: 'deploy',
- },
- startedAt: null,
- queuedDuration: 424850.376278,
- },
- ],
- },
- },
- },
- },
-};
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 99e8ea9d0a4..19f597a7267 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -42,8 +42,8 @@ describe('stage column component', () => {
const findStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const findStageColumnGroup = () => wrapper.find('[data-testid="stage-column-group"]');
const findAllStageColumnGroups = () => wrapper.findAll('[data-testid="stage-column-group"]');
- const findJobItem = () => wrapper.find(JobItem);
- const findActionComponent = () => wrapper.find(ActionComponent);
+ const findJobItem = () => wrapper.findComponent(JobItem);
+ const findActionComponent = () => wrapper.findComponent(ActionComponent);
const createComponent = ({ method = shallowMount, props = {} } = {}) => {
wrapper = method(StageColumnComponent, {
@@ -126,9 +126,9 @@ describe('stage column component', () => {
});
});
- it('capitalizes and escapes name', () => {
- expect(findStageColumnTitle().text()).toBe(
- 'Test &lt;img src=x onerror=alert(document.domain)&gt;',
+ it('escapes name', () => {
+ expect(findStageColumnTitle().html()).toContain(
+ 'test &lt;img src=x onerror=alert(document.domain)&gt;',
);
});
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 44ab60cbee7..e2699d6ff2e 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -6,7 +6,7 @@ import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => {
let wrapper;
- const findLinksInner = () => wrapper.find(LinksInner);
+ const findLinksInner = () => wrapper.findComponent(LinksInner);
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
const containerId = `pipeline-links-container-${pipeline.id}`;
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 859be8d342c..e583c0798f5 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -21,12 +21,12 @@ describe('Pipeline details header', () => {
let glModalDirective;
let mutate = jest.fn();
- const findAlert = () => wrapper.find(GlAlert);
- const findDeleteModal = () => wrapper.find(GlModal);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]');
const findDeleteButton = () => wrapper.find('[data-testid="deletePipeline"]');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const defaultProvideOptions = {
pipelineId: '14',
diff --git a/spec/frontend/pipelines/performance_insights_modal_spec.js b/spec/frontend/pipelines/performance_insights_modal_spec.js
deleted file mode 100644
index 8c802be7718..00000000000
--- a/spec/frontend/pipelines/performance_insights_modal_spec.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import { GlAlert, GlLink, GlModal } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import waitForPromises from 'helpers/wait_for_promises';
-import PerformanceInsightsModal from '~/pipelines/components/performance_insights_modal.vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { trimText } from 'helpers/text_helper';
-import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
-import {
- mockPerformanceInsightsResponse,
- mockPerformanceInsightsNextPageResponse,
-} from './graph/mock_data';
-
-Vue.use(VueApollo);
-
-describe('Performance insights modal', () => {
- let wrapper;
-
- const findModal = () => wrapper.findComponent(GlModal);
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
- const findLimitText = () => wrapper.findByTestId('limit-alert-text');
- const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data');
- const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link');
- const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data');
- const findExecutedCardLink = () => wrapper.findByTestId('insights-executed-card-link');
- const findSlowJobsStage = (index) => wrapper.findAllByTestId('insights-slow-job-stage').at(index);
- const findSlowJobsLink = (index) => wrapper.findAllByTestId('insights-slow-job-link').at(index);
-
- const getPerformanceInsightsHandler = jest
- .fn()
- .mockResolvedValue(mockPerformanceInsightsResponse);
-
- const getPerformanceInsightsNextPageHandler = jest
- .fn()
- .mockResolvedValue(mockPerformanceInsightsNextPageResponse);
-
- const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]];
-
- const createComponent = (handlers = requestHandlers) => {
- wrapper = shallowMountExtended(PerformanceInsightsModal, {
- provide: {
- pipelineIid: '1',
- pipelineProjectPath: 'root/ci-project',
- },
- apolloProvider: createMockApollo(handlers),
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('without next page', () => {
- beforeEach(async () => {
- createComponent();
-
- await waitForPromises();
- });
-
- it('displays modal', () => {
- expect(findModal().exists()).toBe(true);
- });
-
- it('displays alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('displays feedback issue link', () => {
- expect(findLink().text()).toBe('Feedback issue');
- expect(findLink().attributes('href')).toBe(
- 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902',
- );
- });
-
- it('does not display limit text', () => {
- expect(findLimitText().exists()).toBe(false);
- });
-
- describe('queued duration card', () => {
- it('displays card data', () => {
- expect(trimText(findQueuedCardData().text())).toBe('4.9 days');
- });
- it('displays card link', () => {
- expect(findQueuedCardLink().attributes('href')).toBe(
- '/root/lots-of-jobs-project/-/pipelines/98',
- );
- });
- });
-
- describe('executed duration card', () => {
- it('displays card data', () => {
- expect(trimText(findExecutedCardData().text())).toBe('trigger_job');
- });
- it('displays card link', () => {
- expect(findExecutedCardLink().attributes('href')).toBe(
- '/root/lots-of-jobs-project/-/pipelines/98',
- );
- });
- });
-
- describe('slow jobs', () => {
- it.each`
- index | expectedStage | expectedName | expectedLink
- ${0} | ${'build'} | ${'wait_job'} | ${'/root/ci-project/-/jobs/2493'}
- ${1} | ${'deploy'} | ${'artifact_job'} | ${'/root/ci-project/-/jobs/2501'}
- ${2} | ${'test'} | ${'allow_failure_test_job'} | ${'/root/ci-project/-/jobs/2497'}
- ${3} | ${'build'} | ${'large_log_output'} | ${'/root/ci-project/-/jobs/2495'}
- ${4} | ${'build'} | ${'build_job'} | ${'/root/ci-project/-/jobs/2494'}
- `(
- 'should display slow job correctly',
- ({ index, expectedStage, expectedName, expectedLink }) => {
- expect(findSlowJobsStage(index).text()).toBe(expectedStage);
- expect(findSlowJobsLink(index).text()).toBe(expectedName);
- expect(findSlowJobsLink(index).attributes('href')).toBe(expectedLink);
- },
- );
- });
- });
-
- describe('with next page', () => {
- it('displays limit text when there is a next page', async () => {
- createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]);
-
- await waitForPromises();
-
- expect(findLimitText().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index 1b89e322d31..d9199f3b0f7 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -34,7 +34,7 @@ describe('pipeline graph component', () => {
};
const findAlert = () => wrapper.findComponent(GlAlert);
- const findAllJobPills = () => wrapper.findAll(JobPill);
+ const findAllJobPills = () => wrapper.findAllComponents(JobPill);
const findAllStageNames = () => wrapper.findAllComponents(StageName);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index f554166da33..149b40330e2 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -1,12 +1,14 @@
import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineMultiActions, {
i18n,
} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
+import { TRACKING_CATEGORIES } from '~/pipelines/constants';
describe('Pipeline Multi Actions Dropdown', () => {
let wrapper;
@@ -136,4 +138,22 @@ describe('Pipeline Multi Actions Dropdown', () => {
});
});
});
+
+ describe('tracking', () => {
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks artifacts dropdown click', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ createComponent();
+
+ findDropdown().vm.$emit('show');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_artifacts_dropdown', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 25a97ecf49d..1d66607e72b 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,12 +1,15 @@
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { TRACKING_CATEGORIES } from '~/pipelines/constants';
import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
const projectPath = 'test/test';
describe('Pipeline Url Component', () => {
let wrapper;
+ let trackingSpy;
const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell');
const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link');
@@ -14,6 +17,7 @@ describe('Pipeline Url Component', () => {
const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha');
const findCommitIcon = () => wrapper.findByTestId('commit-icon');
const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
+ const findCommitRefName = () => wrapper.findByTestId('commit-ref-name');
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
@@ -31,7 +35,6 @@ describe('Pipeline Url Component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
it('should render pipeline url table cell', () => {
@@ -49,7 +52,7 @@ describe('Pipeline Url Component', () => {
});
it('should render the commit title, commit reference and commit-short-sha', () => {
- createComponent({}, true);
+ createComponent();
const commitWrapper = findCommitTitleContainer();
@@ -83,7 +86,7 @@ describe('Pipeline Url Component', () => {
});
it('should render commit icon tooltip', () => {
- createComponent({}, true);
+ createComponent();
expect(findCommitIcon().attributes('title')).toBe('Commit');
});
@@ -94,8 +97,68 @@ describe('Pipeline Url Component', () => {
${mockPipelineBranch()} | ${'Branch'}
${mockPipeline()} | ${'Merge Request'}
`('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => {
- createComponent(pipeline, true);
+ createComponent(pipeline);
expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
});
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks pipeline id click', () => {
+ createComponent();
+
+ findPipelineUrlLink().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_pipeline_id', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks merge request ref click', () => {
+ createComponent();
+
+ findRefName().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_mr_ref', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks commit ref name click', () => {
+ createComponent(mockPipelineBranch());
+
+ findCommitRefName().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_name', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks commit title click', () => {
+ createComponent(mockPipelineBranch());
+
+ findCommitTitle(findCommitTitleContainer()).vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_title', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks commit short sha click', () => {
+ createComponent(mockPipelineBranch());
+
+ findCommitShortSha().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_sha', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index 9b2ee6b8278..fdfced38dca 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import createFlash from '~/flash';
@@ -9,6 +10,7 @@ import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/flash');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
@@ -29,9 +31,9 @@ describe('Pipelines Actions dropdown', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
- const findAllCountdowns = () => wrapper.findAll(GlCountdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown);
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -96,6 +98,22 @@ describe('Pipelines Actions dropdown', () => {
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
+
+ describe('tracking', () => {
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks manual actions click', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('shown');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+ });
});
describe('scheduled jobs', () => {
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 2d876841e06..e3e54716a7b 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -30,8 +30,9 @@ describe('Pipelines Artifacts dropdown', () => {
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem);
- const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem);
+ const findFirstGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findAllGlDropdownItems = () =>
+ wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 0bed24e588e..cc2ff90de57 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -7,6 +7,7 @@ import { nextTick } from 'vue';
import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -16,7 +17,7 @@ import NavigationControls from '~/pipelines/components/pipelines_list/nav_contro
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
-import { RAW_TEXT_WARNING } from '~/pipelines/constants';
+import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
@@ -37,6 +38,7 @@ const mockPipelineWithStages = mockPipelinesResponse.pipelines.find(
describe('Pipelines', () => {
let wrapper;
let mock;
+ let trackingSpy;
const paths = {
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
@@ -123,7 +125,7 @@ describe('Pipelines', () => {
});
it('shows loading state when the app is loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not display tabs when the first request has not yet been made', () => {
@@ -236,6 +238,8 @@ describe('Pipelines', () => {
count: mockPipelinesResponse.count,
});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
goToTab('finished');
await waitForPromises();
@@ -256,6 +260,12 @@ describe('Pipelines', () => {
`${window.location.pathname}?scope=finished&page=1`,
);
});
+
+ it('tracks tab change click', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filter_tabs', {
+ label: TRACKING_CATEGORIES.tabs,
+ });
+ });
});
describe('when the scope in the tab is empty', () => {
@@ -375,7 +385,7 @@ describe('Pipelines', () => {
const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize);
const goToPage = (page) => {
- findTablePagination().find(GlPagination).vm.$emit('input', page);
+ findTablePagination().findComponent(GlPagination).vm.$emit('input', page);
};
beforeEach(async () => {
@@ -583,7 +593,7 @@ describe('Pipelines', () => {
'This project is not currently set up to run pipelines.',
);
- expect(findEmptyState().find(GlButton).exists()).toBe(false);
+ expect(findEmptyState().findComponent(GlButton).exists()).toBe(false);
});
it('does not render tabs or buttons', () => {
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 7b49baa5a20..044683ce533 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -2,8 +2,9 @@ import '~/commons';
import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
@@ -13,6 +14,7 @@ import {
PipelineKeyOptions,
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
+ TRACKING_CATEGORIES,
} from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub';
@@ -23,6 +25,7 @@ jest.mock('~/pipelines/event_hub');
describe('Pipelines Table', () => {
let pipeline;
let wrapper;
+ let trackingSpy;
const defaultProps = {
pipelines: [],
@@ -69,6 +72,7 @@ describe('Pipelines Table', () => {
afterEach(() => {
wrapper.destroy();
+
wrapper = null;
});
@@ -96,10 +100,6 @@ describe('Pipelines Table', () => {
it('should render a status badge', () => {
expect(findStatusBadge().exists()).toBe(true);
});
-
- it('should render status badge with correct path', () => {
- expect(findStatusBadge().attributes('href')).toBe(pipeline.path);
- });
});
describe('pipeline cell', () => {
@@ -113,40 +113,28 @@ describe('Pipelines Table', () => {
});
describe('stages cell', () => {
- it('should render a pipeline mini graph', () => {
+ it('should render pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
});
it('should render the right number of stages', () => {
const stagesLength = pipeline.details.stages.length;
- expect(
- findPipelineMiniGraph().findAll('[data-testid="mini-pipeline-graph-dropdown"]'),
- ).toHaveLength(stagesLength);
+ expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
- pipeline.details.stages = null;
+ pipeline.details.stages = [];
createComponent({ pipelines: [pipeline] });
});
it('stages are not rendered', () => {
- expect(findPipelineMiniGraph().exists()).toBe(false);
+ expect(findPipelineMiniGraph().props('stages')).toHaveLength(0);
});
});
- it('should not update dropdown', () => {
- expect(findPipelineMiniGraph().props('updateDropdown')).toBe(false);
- });
-
- it('when update graph dropdown is set, should update graph dropdown', () => {
- createComponent({ pipelines: [pipeline], updateGraphDropdown: true });
-
- expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true);
- });
-
it('when action request is complete, should refresh table', () => {
findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete');
@@ -179,5 +167,47 @@ describe('Pipelines Table', () => {
expect(findTriggerer().exists()).toBe(true);
});
});
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks status badge click', () => {
+ findStatusBadge().vm.$emit('ciStatusBadgeClick');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks retry pipeline button click', () => {
+ findRetryBtn().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks cancel pipeline button click', () => {
+ findCancelBtn().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+
+ it('tracks pipeline mini graph stage click', () => {
+ findPipelineMiniGraph().vm.$emit('miniGraphStageClick');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index c372ac06c35..da13df833e7 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -26,10 +26,10 @@ describe('Test reports suite table', () => {
const noCasesMessage = () => wrapper.findByTestId('no-test-cases');
const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired');
- const artifactsExpiredEmptyState = () => wrapper.find(GlEmptyState);
+ const artifactsExpiredEmptyState = () => wrapper.findComponent(GlEmptyState);
const allCaseRows = () => wrapper.findAllByTestId('test-case-row');
const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index);
- const findLinkForRow = (row) => row.find(GlLink);
+ const findLinkForRow = (row) => row.findComponent(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = ({ suite = testSuite, perPage = 20, errorMessage } = {}) => {
@@ -113,7 +113,7 @@ describe('Test reports suite table', () => {
const filePath = `${blobPath}/${relativeFile}`;
const row = findCaseRowAtIndex(0);
const fileLink = findLinkForRow(row);
- const button = row.find(GlButton);
+ const button = row.findComponent(GlButton);
expect(fileLink.attributes('href')).toBe(filePath);
expect(row.text()).toContain(file);
@@ -134,7 +134,7 @@ describe('Test reports suite table', () => {
});
it('renders a pagination component', () => {
- expect(wrapper.find(GlPagination).exists()).toBe(true);
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
index 0e1229f7067..cfe9ff564dc 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -44,7 +44,7 @@ describe('Test reports summary table', () => {
describe('when test reports are supplied', () => {
beforeEach(() => createComponent());
- const findErrorIcon = () => wrapper.find({ ref: 'suiteErrorIcon' });
+ const findErrorIcon = () => wrapper.findComponent({ ref: 'suiteErrorIcon' });
it('renders the correct number of rows', () => {
expect(noSuitesToShow().exists()).toBe(false);
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index 3de7995b476..f0da0df2ba6 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -48,7 +48,7 @@ describe('Timeago component', () => {
});
it('should render duration and timer svg', () => {
- const icon = duration().find(GlIcon);
+ const icon = duration().findComponent(GlIcon);
expect(duration().exists()).toBe(true);
expect(icon.props('name')).toBe('timer');
@@ -71,7 +71,7 @@ describe('Timeago component', () => {
});
it('should render time and calendar icon', () => {
- const icon = finishedAt().find(GlIcon);
+ const icon = finishedAt().findComponent(GlIcon);
const time = finishedAt().find('time');
expect(finishedAt().exists()).toBe(true);
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index ba478363d04..caa66502e11 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -9,9 +9,10 @@ import { branches, mockBranchesAfterMap } from '../mock_data';
describe('Pipeline Branch Name Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const getBranchSuggestions = () =>
findAllFilteredSearchSuggestions().wrappers.map((w) => w.text());
diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
index b8abf2c1727..60abb63a7e0 100644
--- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
@@ -7,8 +7,9 @@ import PipelineSourceToken from '~/pipelines/components/pipelines_list/tokens/pi
describe('Pipeline Source Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
const defaultProps = {
config: {
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index 2c5fa8b00e2..94f9a37f707 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -6,9 +6,10 @@ import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pi
describe('Pipeline Status Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
- const findAllGlIcons = () => wrapper.findAll(GlIcon);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findAllGlIcons = () => wrapper.findAllComponents(GlIcon);
const defaultProps = {
config: {
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
index 596a9218c39..7311a5d2f5a 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -7,9 +7,10 @@ import { tags, mockTagsAfterMap } from '../mock_data';
describe('Pipeline Branch Name Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const stubs = {
GlFilteredSearchToken: {
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index 397dbdf95a9..c763bfe1b27 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -8,9 +8,10 @@ import { users } from '../mock_data';
describe('Pipeline Trigger Author Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const defaultProps = {
config: {
diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js
index a82390fae22..1c23a7e4fcf 100644
--- a/spec/frontend/pipelines/utils_spec.js
+++ b/spec/frontend/pipelines/utils_spec.js
@@ -8,14 +8,10 @@ import {
removeOrphanNodes,
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
-import { createNodeDict, calculateJobStats, calculateSlowestFiveJobs } from '~/pipelines/utils';
+import { createNodeDict } from '~/pipelines/utils';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
-import {
- generateResponse,
- mockPipelineResponse,
- mockPerformanceInsightsResponse,
-} from './graph/mock_data';
+import { generateResponse, mockPipelineResponse } from './graph/mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
@@ -162,40 +158,4 @@ describe('DAG visualization parsing utilities', () => {
expect(columns).toMatchSnapshot();
});
});
-
- describe('performance insights', () => {
- const {
- data: {
- project: {
- pipeline: { jobs },
- },
- },
- } = mockPerformanceInsightsResponse;
-
- describe('calculateJobStats', () => {
- const expectedJob = jobs.nodes[0];
-
- it('returns the job that spent this longest time queued', () => {
- expect(calculateJobStats(jobs, 'queuedDuration')).toEqual(expectedJob);
- });
-
- it('returns the job that was executed last', () => {
- expect(calculateJobStats(jobs, 'startedAt')).toEqual(expectedJob);
- });
- });
-
- describe('calculateSlowestFiveJobs', () => {
- it('returns the slowest five jobs of the pipeline', () => {
- const expectedJobs = [
- jobs.nodes[9],
- jobs.nodes[1],
- jobs.nodes[5],
- jobs.nodes[7],
- jobs.nodes[8],
- ];
-
- expect(calculateSlowestFiveJobs(jobs)).toEqual(expectedJobs);
- });
- });
- });
});
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index 6fdcd34ae83..eba6b95214d 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -31,7 +31,7 @@ describe('popovers/components/popovers.vue', () => {
return target;
};
- const allPopovers = () => wrapper.findAll(GlPopover);
+ const allPopovers = () => wrapper.findAllComponents(GlPopover);
afterEach(() => {
wrapper.destroy();
@@ -42,7 +42,7 @@ describe('popovers/components/popovers.vue', () => {
it('attaches popovers to the targets specified', async () => {
const target = createPopoverTarget();
await buildWrapper(target);
- expect(wrapper.find(GlPopover).props('target')).toBe(target);
+ expect(wrapper.findComponent(GlPopover).props('target')).toBe(target);
});
it('does not attach a popover twice to the same element', async () => {
@@ -52,7 +52,7 @@ describe('popovers/components/popovers.vue', () => {
await nextTick();
- expect(wrapper.findAll(GlPopover)).toHaveLength(1);
+ expect(wrapper.findAllComponents(GlPopover)).toHaveLength(1);
});
describe('supports HTML content', () => {
@@ -66,7 +66,7 @@ describe('popovers/components/popovers.vue', () => {
`('$description', async ({ content, render }) => {
await buildWrapper(createPopoverTarget({ content, html: true }));
- const html = wrapper.find(GlPopover).html();
+ const html = wrapper.findComponent(GlPopover).html();
expect(html).toContain(render);
});
});
@@ -78,7 +78,7 @@ describe('popovers/components/popovers.vue', () => {
`('sets $option to $value when data-$option is set in target', async ({ option, value }) => {
await buildWrapper(createPopoverTarget({ [option]: value }));
- expect(wrapper.find(GlPopover).props(option)).toBe(value);
+ expect(wrapper.findComponent(GlPopover).props(option)).toBe(value);
});
});
diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js
index ad62d84c43c..e4a316e1ee7 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -53,7 +53,7 @@ describe('DeleteAccountModal component', () => {
input: vm.$el.querySelector(`[name="${confirmation}"]`),
};
};
- const findModal = () => wrapper.find(GlModalStub);
+ const findModal = () => wrapper.findComponent(GlModalStub);
describe('with password confirmation', () => {
beforeEach(async () => {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index 0e56bccf27e..e331eed1863 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -44,7 +44,7 @@ describe('UpdateUsername component', () => {
});
const findElements = () => {
- const modal = wrapper.find(GlModal);
+ const modal = wrapper.findComponent(GlModal);
return {
modal,
@@ -149,7 +149,7 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
- expect(createFlash).toBeCalledWith({
+ expect(createFlash).toHaveBeenCalledWith({
message: 'Invalid username',
});
});
@@ -161,7 +161,7 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
- expect(createFlash).toBeCalledWith({
+ expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while updating your username, please try again.',
});
});
diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js
index 92c53b8c91b..f650bee7fda 100644
--- a/spec/frontend/profile/preferences/components/integration_view_spec.js
+++ b/spec/frontend/profile/preferences/components/integration_view_spec.js
@@ -98,6 +98,6 @@ describe('IntegrationView component', () => {
it('should render the help text', () => {
wrapper = createComponent();
- expect(wrapper.find(IntegrationHelpText).exists()).toBe(true);
+ expect(wrapper.findComponent(IntegrationHelpText).exists()).toBe(true);
});
});
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 4d2dcf83d3b..89ce838a383 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -90,7 +90,7 @@ describe('ProfilePreferences component', () => {
it('should not render Integrations section', () => {
wrapper = createComponent();
- const views = wrapper.findAll(IntegrationView);
+ const views = wrapper.findAllComponents(IntegrationView);
const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
@@ -103,7 +103,7 @@ describe('ProfilePreferences component', () => {
wrapper = createComponent({ provide: { integrationViews } });
const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
- const views = wrapper.findAll(IntegrationView);
+ const views = wrapper.findAllComponents(IntegrationView);
expect(divider.exists()).toBe(true);
expect(heading.exists()).toBe(true);
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 79e9dab935d..20c312ec771 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -99,7 +99,9 @@ describe('CommitFormModal', () => {
createComponent(shallowMount, {}, { prependedText: '_prepended_text_' });
expect(findPrependedText().exists()).toBe(true);
- expect(findPrependedText().find(GlSprintf).attributes('message')).toBe('_prepended_text_');
+ expect(findPrependedText().findComponent(GlSprintf).attributes('message')).toBe(
+ '_prepended_text_',
+ );
});
it('Does not show prepended text', () => {
@@ -124,7 +126,7 @@ describe('CommitFormModal', () => {
createComponent(shallowMount, { pushCode: false });
expect(findAppendedText().exists()).toBe(true);
- expect(findAppendedText().find(GlSprintf).attributes('message')).toContain(
+ expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain(
mockData.modalPropsData.i18n.branchInFork,
);
});
@@ -133,7 +135,7 @@ describe('CommitFormModal', () => {
createComponent(shallowMount, { pushCode: false, branchCollaboration: true });
expect(findAppendedText().exists()).toBe(true);
- expect(findAppendedText().find(GlSprintf).attributes('message')).toContain(
+ expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain(
mockData.modalPropsData.i18n.existingBranch,
);
});
diff --git a/spec/frontend/projects/commit/store/mutations_spec.js b/spec/frontend/projects/commit/store/mutations_spec.js
index 60abf0fddad..40174b3057a 100644
--- a/spec/frontend/projects/commit/store/mutations_spec.js
+++ b/spec/frontend/projects/commit/store/mutations_spec.js
@@ -26,7 +26,7 @@ describe('Commit form modal mutations', () => {
});
describe('CLEAR_MODAL', () => {
- it('should clear modal state ', () => {
+ it('should clear modal state', () => {
stateCopy = { branch: '_main_', defaultBranch: '_default_branch_' };
mutations[types.CLEAR_MODAL](stateCopy);
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 57e5ef0ed1d..907e0e226b6 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -58,11 +58,11 @@ describe('Author Select', () => {
resetHTMLFixture();
});
- const findDropdownContainer = () => wrapper.find({ ref: 'dropdownContainer' });
- const findDropdown = () => wrapper.find(GlDropdown);
- const findDropdownHeader = () => wrapper.find(GlDropdownSectionHeader);
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownContainer = () => wrapper.findComponent({ ref: 'dropdownContainer' });
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
describe('user is searching via "filter by commit message"', () => {
it('disables dropdown container', async () => {
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index c9ffdf20c32..2dbecf7cc61 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -58,7 +58,7 @@ describe('CompareApp component', () => {
});
it('render Source and Target BranchDropdown components', () => {
- const revisionCards = wrapper.findAll(RevisionCard);
+ const revisionCards = wrapper.findAllComponents(RevisionCard);
expect(revisionCards.length).toBe(2);
expect(revisionCards.at(0).props('revisionText')).toBe('Source');
@@ -66,7 +66,7 @@ describe('CompareApp component', () => {
});
describe('compare button', () => {
- const findCompareButton = () => wrapper.find(GlButton);
+ const findCompareButton = () => wrapper.findComponent(GlButton);
it('renders button', () => {
expect(findCompareButton().exists()).toBe(true);
diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
index 98aec347e4b..21cca857c6a 100644
--- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
@@ -21,7 +21,7 @@ describe('RepoDropdown component', () => {
wrapper = null;
});
- const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
describe('Source Revision', () => {
@@ -73,7 +73,7 @@ describe('RepoDropdown component', () => {
});
it('emits `selectProject` event when another target project is selected', async () => {
- findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click');
+ findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click');
await nextTick();
expect(wrapper.emitted('selectProject')[0][0]).toEqual({
diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js
index a741393fcf3..b23bd91ceda 100644
--- a/spec/frontend/projects/compare/components/revision_card_spec.js
+++ b/spec/frontend/projects/compare/components/revision_card_spec.js
@@ -32,10 +32,10 @@ describe('RepoDropdown component', () => {
});
it('renders RepoDropdown component', () => {
- expect(wrapper.findAll(RepoDropdown).exists()).toBe(true);
+ expect(wrapper.findAllComponents(RepoDropdown).exists()).toBe(true);
});
it('renders RevisionDropdown component', () => {
- expect(wrapper.findAll(RevisionDropdown).exists()).toBe(true);
+ expect(wrapper.findAllComponents(RevisionDropdown).exists()).toBe(true);
});
});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index 102f95f65da..f64af1aa994 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -38,7 +38,7 @@ describe('RevisionDropdown component', () => {
axiosMock.restore();
});
- const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
it('sets hidden input', () => {
expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe(
@@ -99,7 +99,7 @@ describe('RevisionDropdown component', () => {
});
it('emits a "selectRevision" event when a revision is selected', async () => {
- const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index c8a90848492..35e32fd3da0 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -35,8 +35,8 @@ describe('RevisionDropdown component', () => {
axiosMock.restore();
});
- const findGlDropdown = () => wrapper.find(GlDropdown);
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
it('sets hidden input', () => {
createComponent();
@@ -144,7 +144,7 @@ describe('RevisionDropdown component', () => {
wrapper.vm.branches = ['some-branch'];
await nextTick();
- findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click');
+ findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click');
expect(wrapper.emitted('selectRevision')[0][0]).toEqual({
direction: 'to',
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
index a3bc4931eb3..49e3218e5bc 100644
--- a/spec/frontend/projects/components/project_delete_button_spec.js
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -8,7 +8,7 @@ jest.mock('lodash/uniqueId', () => () => 'fakeUniqueId');
describe('Project remove modal', () => {
let wrapper;
- const findSharedDeleteButton = () => wrapper.find(SharedDeleteButton);
+ const findSharedDeleteButton = () => wrapper.findComponent(SharedDeleteButton);
const defaultProps = {
confirmPhrase: 'foo',
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
index 45c39ee91d8..097b18025a3 100644
--- a/spec/frontend/projects/components/shared/delete_button_spec.js
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -11,7 +11,7 @@ describe('Project remove modal', () => {
const findFormElement = () => wrapper.find('form');
const findConfirmButton = () => wrapper.find('.js-modal-action-primary');
const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
const findTitle = () => wrapper.find('[data-testid="delete-alert-title"]');
const findAlertBody = () => wrapper.find('[data-testid="delete-alert-body"]');
diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js
index d7308963088..50638755260 100644
--- a/spec/frontend/projects/details/upload_button_spec.js
+++ b/spec/frontend/projects/details/upload_button_spec.js
@@ -32,11 +32,11 @@ describe('UploadButton', () => {
});
it('displays an upload button', () => {
- expect(wrapper.find(GlButton).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(true);
});
it('contains a modal', () => {
- const modal = wrapper.find(UploadBlobModal);
+ const modal = wrapper.findComponent(UploadBlobModal);
expect(modal.exists()).toBe(true);
expect(modal.props('modalId')).toBe(MODAL_ID);
@@ -44,7 +44,7 @@ describe('UploadButton', () => {
describe('when clickinig the upload file button', () => {
beforeEach(() => {
- wrapper.find(GlButton).vm.$emit('click');
+ wrapper.findComponent(GlButton).vm.$emit('click');
});
it('opens the modal', () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 7b9011fa3d9..e3aaf760d1e 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -47,15 +47,16 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper.destroy();
});
- const findGlTabs = () => wrapper.find(GlTabs);
- const findAllGlTabs = () => wrapper.findAll(GlTab);
+ const findGlTabs = () => wrapper.findComponent(GlTabs);
+ const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
- const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub);
- const findTimeToRestoreServiceCharts = () => wrapper.find(TimeToRestoreServiceChartsStub);
- const findChangeFailureRateCharts = () => wrapper.find(ChangeFailureRateChartsStub);
- const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
- const findPipelineCharts = () => wrapper.find(PipelineCharts);
- const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub);
+ const findLeadTimeCharts = () => wrapper.findComponent(LeadTimeChartsStub);
+ const findTimeToRestoreServiceCharts = () =>
+ wrapper.findComponent(TimeToRestoreServiceChartsStub);
+ const findChangeFailureRateCharts = () => wrapper.findComponent(ChangeFailureRateChartsStub);
+ const findDeploymentFrequencyCharts = () => wrapper.findComponent(DeploymentFrequencyChartsStub);
+ const findPipelineCharts = () => wrapper.findComponent(PipelineCharts);
+ const findProjectQualitySummary = () => wrapper.findComponent(ProjectQualitySummaryStub);
describe('when all charts are available', () => {
beforeEach(() => {
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
index 7bb289408b8..8c18d2992ea 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -81,7 +81,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
it('should select a different chart on change', async () => {
findSegmentedControl().vm.$emit('input', 1);
- const chart = wrapper.find(CiCdAnalyticsAreaChart);
+ const chart = wrapper.findComponent(CiCdAnalyticsAreaChart);
await nextTick();
@@ -92,7 +92,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
it('should not display charts if there are no charts', () => {
wrapper = createWrapper({ charts: [] });
- expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false);
+ expect(wrapper.findComponent(CiCdAnalyticsAreaChart).exists()).toBe(false);
});
describe('slots', () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 3c91b913e67..8fb59f38ee1 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -44,7 +44,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
describe('overall statistics', () => {
it('displays the statistics list', () => {
- const list = wrapper.find(StatisticsList);
+ const list = wrapper.findComponent(StatisticsList);
expect(list.exists()).toBe(true);
expect(list.props('counts')).toEqual({
@@ -56,9 +56,9 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
});
it('displays the commit duration chart', () => {
- const chart = wrapper.find(GlColumnChart);
+ const chart = wrapper.findComponent(GlColumnChart);
- expect(chart.exists()).toBeTruthy();
+ expect(chart.exists()).toBe(true);
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
@@ -68,12 +68,12 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
describe('pipelines charts', () => {
it('displays the charts components', () => {
- expect(wrapper.find(CiCdAnalyticsCharts).exists()).toBe(true);
+ expect(wrapper.findComponent(CiCdAnalyticsCharts).exists()).toBe(true);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
- const charts = wrapper.find(CiCdAnalyticsCharts);
+ const charts = wrapper.findComponent(CiCdAnalyticsCharts);
expect(charts.props()).toEqual({
charts: wrapper.vm.areaCharts,
chartOptions: wrapper.vm.$options.areaChartOptions,
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
index 1db48ce05d7..1b06f7874a3 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -134,7 +134,7 @@ describe('Access Level Dropdown', () => {
await waitForPromises();
});
- it('renders headers for each section ', () => {
+ it('renders headers for each section', () => {
expect(findAllDropdownHeaders()).toHaveLength(4);
});
@@ -164,7 +164,7 @@ describe('Access Level Dropdown', () => {
expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!');
});
- it('when no items selected, displays a default fallback label and has default CSS class ', () => {
+ it('when no items selected, displays a default fallback label and has default CSS class', () => {
expect(findDropdownToggleLabel()).toBe(i18n.selectUsers);
expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!');
});
@@ -217,7 +217,7 @@ describe('Access Level Dropdown', () => {
});
describe('selecting an item', () => {
- it('selects the item on click and deselects on the next click ', async () => {
+ it('selects the item on click and deselects on the next click', async () => {
createComponent();
await waitForPromises();
@@ -230,7 +230,7 @@ describe('Access Level Dropdown', () => {
expect(item.props('isChecked')).toBe(false);
});
- it('emits a formatted update on selection ', async () => {
+ it('emits a formatted update on selection', async () => {
// ids: the items appear in that order in the dropdown
// 1 2 3 - roles
// 4 5 6 - groups
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index 0a05832ceb6..329060b9d10 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -27,9 +27,9 @@ describe('projects/settings/components/shared_runners', () => {
});
};
- const findErrorAlert = () => wrapper.find(GlAlert);
- const findSharedRunnersToggle = () => wrapper.find(GlToggle);
- const findToggleTooltip = () => wrapper.find(GlTooltip);
+ const findErrorAlert = () => wrapper.findComponent(GlAlert);
+ const findSharedRunnersToggle = () => wrapper.findComponent(GlToggle);
+ const findToggleTooltip = () => wrapper.findComponent(GlTooltip);
const getToggleValue = () => findSharedRunnersToggle().props('value');
const isToggleLoading = () => findSharedRunnersToggle().props('isLoading');
const isToggleDisabled = () => findSharedRunnersToggle().props('disabled');
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index e12c3aeedd6..e920cd48163 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -1,18 +1,55 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
+import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
+import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
+import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
+import createFlash from '~/flash';
+import { branchRulesMockResponse, propsDataMock } from './mock_data';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
describe('Branch rules app', () => {
let wrapper;
+ let fakeApollo;
+
+ const branchRulesQuerySuccessHandler = jest.fn().mockResolvedValue(branchRulesMockResponse);
+
+ const createComponent = async ({ queryHandler = branchRulesQuerySuccessHandler } = {}) => {
+ fakeApollo = createMockApollo([[branchRulesQuery, queryHandler]]);
+
+ wrapper = mountExtended(BranchRules, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ ...propsDataMock,
+ },
+ });
- const createComponent = () => {
- wrapper = mountExtended(BranchRules);
+ await waitForPromises();
};
- const findTitle = () => wrapper.find('strong');
+ const findAllBranchRules = () => wrapper.findAllComponents(BranchRule);
+ const findEmptyState = () => wrapper.findByTestId('empty');
beforeEach(() => createComponent());
- it('renders a title', () => {
- expect(findTitle().text()).toBe('Branch');
+ it('displays an error if branch rules query fails', async () => {
+ await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
+ expect(createFlash).toHaveBeenCalledWith({ message: i18n.queryError });
+ });
+
+ it('displays an empty state if no branch rules are present', async () => {
+ await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
+ expect(findEmptyState().text()).toBe(i18n.emptyState);
+ });
+
+ it('renders branch rules', () => {
+ const { nodes } = branchRulesMockResponse.data.project.branchRules;
+ expect(findAllBranchRules().at(0).text()).toBe(nodes[0].name);
+ expect(findAllBranchRules().at(1).text()).toBe(nodes[1].name);
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
new file mode 100644
index 00000000000..924dab60704
--- /dev/null
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -0,0 +1,58 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BranchRule, {
+ i18n,
+} from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
+
+const defaultProps = {
+ name: 'main',
+ isDefault: true,
+ isProtected: true,
+ approvalDetails: ['requires approval from TEST', '2 status checks'],
+};
+
+describe('Branch rule', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(BranchRule, { propsData: { ...defaultProps, ...props } });
+ };
+
+ const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel);
+ const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel);
+ const findBranchName = () => wrapper.findByText(defaultProps.name);
+ const findProtectionDetailsList = () => wrapper.findByRole('list');
+ const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem');
+
+ beforeEach(() => createComponent());
+
+ it('renders the branch name', () => {
+ expect(findBranchName().exists()).toBe(true);
+ });
+
+ describe('badges', () => {
+ it('renders both default and protected badges', () => {
+ expect(findDefaultBadge().exists()).toBe(true);
+ expect(findProtectedBadge().exists()).toBe(true);
+ });
+
+ it('does not render default badge if isDefault is set to false', () => {
+ createComponent({ isDefault: false });
+ expect(findDefaultBadge().exists()).toBe(false);
+ });
+
+ it('does not render protected badge if isProtected is set to false', () => {
+ createComponent({ isProtected: false });
+ expect(findProtectedBadge().exists()).toBe(false);
+ });
+ });
+
+ it('does not render the protection details list of no details are present', () => {
+ createComponent({ approvalDetails: null });
+ expect(findProtectionDetailsList().exists()).toBe(false);
+ });
+
+ it('renders the protection details list items', () => {
+ expect(findProtectionDetailsListItems().at(0).text()).toBe(defaultProps.approvalDetails[0]);
+ expect(findProtectionDetailsListItems().at(1).text()).toBe(defaultProps.approvalDetails[1]);
+ });
+});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
new file mode 100644
index 00000000000..14ed35f047d
--- /dev/null
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -0,0 +1,25 @@
+export const branchRulesMockResponse = {
+ data: {
+ project: {
+ id: '123',
+ __typename: 'Project',
+ branchRules: {
+ __typename: 'BranchRuleConnection',
+ nodes: [
+ {
+ name: 'main',
+ __typename: 'BranchRule',
+ },
+ {
+ name: 'test-*',
+ __typename: 'BranchRule',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const propsDataMock = {
+ projectPath: 'some/project/path',
+};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 62224612387..13f3eea277a 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
@@ -23,11 +23,16 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
+ publicProject: false,
};
- const getAlertText = () => wrapper.find(GlAlert).text();
+ const getAlertText = () => wrapper.findComponent(GlAlert).text();
- const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData });
+ const createComponent = (customInject = {}) =>
+ shallowMount(ServiceDeskRoot, {
+ provide: { ...provideData, ...customInject },
+ stubs: { GlSprintf },
+ });
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
@@ -46,7 +51,7 @@ describe('ServiceDeskRoot', () => {
it('is rendered', () => {
wrapper = createComponent();
- expect(wrapper.find(ServiceDeskSetting).props()).toEqual({
+ expect(wrapper.findComponent(ServiceDeskSetting).props()).toEqual({
customEmail: provideData.customEmail,
customEmailEnabled: provideData.customEmailEnabled,
incomingEmail: provideData.initialIncomingEmail,
@@ -60,12 +65,31 @@ describe('ServiceDeskRoot', () => {
});
});
+ it('shows alert about email inference when current project is public', () => {
+ wrapper = createComponent({
+ publicProject: true,
+ });
+
+ const alertEl = wrapper.find('[data-testid="public-project-alert"]');
+ expect(alertEl.exists()).toBe(true);
+ expect(alertEl.text()).toContain(
+ 'This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name.',
+ );
+
+ const alertBodyLink = alertEl.findComponent(GlLink);
+ expect(alertBodyLink.exists()).toBe(true);
+ expect(alertBodyLink.attributes('href')).toBe(
+ '/help/user/project/service_desk.html#using-a-custom-email-address',
+ );
+ expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
+ });
+
describe('toggle event', () => {
describe('when toggling service desk on', () => {
beforeEach(async () => {
wrapper = createComponent();
- wrapper.find(ServiceDeskSetting).vm.$emit('toggle', true);
+ wrapper.findComponent(ServiceDeskSetting).vm.$emit('toggle', true);
await waitForPromises();
});
@@ -87,7 +111,7 @@ describe('ServiceDeskRoot', () => {
beforeEach(async () => {
wrapper = createComponent();
- wrapper.find(ServiceDeskSetting).vm.$emit('toggle', false);
+ wrapper.findComponent(ServiceDeskSetting).vm.$emit('toggle', false);
await waitForPromises();
});
@@ -119,7 +143,7 @@ describe('ServiceDeskRoot', () => {
projectKey: 'key',
};
- wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
+ wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload);
await waitForPromises();
});
@@ -150,7 +174,7 @@ describe('ServiceDeskRoot', () => {
projectKey: 'key',
};
- wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
+ wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload);
await waitForPromises();
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index aac1a418142..7c3f4e76ae5 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -8,13 +8,14 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('ServiceDeskSetting', () => {
let wrapper;
- const findButton = () => wrapper.find(GlButton);
- const findClipboardButton = () => wrapper.find(ClipboardButton);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-label');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findTemplateDropdown = () => wrapper.find(GlDropdown);
- const findToggle = () => wrapper.find(GlToggle);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTemplateDropdown = () => wrapper.findComponent(GlDropdown);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findSuffixFormGroup = () => wrapper.findByTestId('suffix-form-group');
const createComponent = ({ props = {} } = {}) =>
extendedWrapper(
@@ -51,6 +52,32 @@ describe('ServiceDeskSetting', () => {
expect(findLoadingIcon().exists()).toBe(true);
expect(findIncomingEmail().exists()).toBe(false);
});
+
+ it('should display help text', () => {
+ expect(findSuffixFormGroup().text()).toContain(
+ 'To add a custom suffix, set up a Service Desk email address',
+ );
+ expect(findSuffixFormGroup().text()).not.toContain(
+ 'Add a suffix to Service Desk email address',
+ );
+ });
+ });
+ });
+
+ describe('when customEmailEnabled', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: { customEmailEnabled: true },
+ });
+ });
+
+ it('should not display help text', () => {
+ expect(findSuffixFormGroup().text()).not.toContain(
+ 'To add a custom suffix, set up a Service Desk email address',
+ );
+ expect(findSuffixFormGroup().text()).toContain(
+ 'Add a suffix to Service Desk email address',
+ );
});
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
index cdb355f5a9b..6adcfbe8157 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
@@ -7,7 +7,7 @@ import { TEMPLATES } from './mock_data';
describe('ServiceDeskTemplateDropdown', () => {
let wrapper;
- const findTemplateDropdown = () => wrapper.find(GlDropdown);
+ const findTemplateDropdown = () => wrapper.findComponent(GlDropdown);
const createComponent = ({ props = {} } = {}) =>
extendedWrapper(
@@ -53,7 +53,7 @@ describe('ServiceDeskTemplateDropdown', () => {
props: { templates: TEMPLATES },
});
- const headerItems = wrapper.findAll(GlDropdownSectionHeader);
+ const headerItems = wrapper.findAllComponents(GlDropdownSectionHeader);
expect(headerItems).toHaveLength(1);
expect(headerItems.at(0).text()).toBe(TEMPLATES[0]);
@@ -68,7 +68,7 @@ describe('ServiceDeskTemplateDropdown', () => {
const expectedTemplates = templates[1];
- const items = wrapper.findAll(GlDropdownItem);
+ const items = wrapper.findAllComponents(GlDropdownItem);
const dropdownList = expectedTemplates.map((_, index) => items.at(index).text());
expect(items).toHaveLength(expectedTemplates.length);
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 882cb2c1199..6c5af5a2625 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -93,20 +93,20 @@ describe('Ref selector component', () => {
const findNoResults = () => wrapper.find('[data-testid="no-results"]');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
- const findBranchDropdownItems = () => findBranchesSection().findAll(GlDropdownItem);
+ const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
- const findTagDropdownItems = () => findTagsSection().findAll(GlDropdownItem);
+ const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
- const findCommitDropdownItems = () => findCommitsSection().findAll(GlDropdownItem);
+ const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
//
@@ -530,13 +530,13 @@ describe('Ref selector component', () => {
});
it('renders a checkmark by the selected item', async () => {
- expect(findFirstBranchDropdownItem().find(GlIcon).element).toHaveClass(
+ expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass(
'gl-visibility-hidden',
);
await selectFirstBranch();
- expect(findFirstBranchDropdownItem().find(GlIcon).element).not.toHaveClass(
+ expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass(
'gl-visibility-hidden',
);
});
@@ -684,7 +684,8 @@ describe('Ref selector component', () => {
describe('validation state', () => {
const invalidClass = 'gl-inset-border-1-red-500!';
- const isInvalidClassApplied = () => wrapper.find(GlDropdown).props('toggleClass')[invalidClass];
+ const isInvalidClassApplied = () =>
+ wrapper.findComponent(GlDropdown).props('toggleClass')[invalidClass];
describe('valid state', () => {
describe('when the state prop is not provided', () => {
diff --git a/spec/frontend/related_issues/components/related_issuable_input_spec.js b/spec/frontend/related_issues/components/related_issuable_input_spec.js
index 7d11e3cffb0..f6a13856042 100644
--- a/spec/frontend/related_issues/components/related_issuable_input_spec.js
+++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js
@@ -33,7 +33,7 @@ describe('RelatedIssuableInput', () => {
it('shows placeholder text', () => {
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
- expect(wrapper.find({ ref: 'input' }).element.placeholder).toBe(
+ expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
'Paste issue link or <#issue id>',
);
});
@@ -54,7 +54,7 @@ describe('RelatedIssuableInput', () => {
},
});
- expect(wrapper.find({ ref: 'input' }).element.value).toBe('');
+ expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
});
it('does not have GfmAutoComplete', () => {
@@ -85,7 +85,7 @@ describe('RelatedIssuableInput', () => {
await nextTick();
- expect(document.activeElement).toBe(wrapper.find({ ref: 'input' }).element);
+ expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
});
});
@@ -100,7 +100,7 @@ describe('RelatedIssuableInput', () => {
const newInputValue = 'filling in things';
const untouchedRawReferences = newInputValue.trim().split(/\s/);
const touchedReference = untouchedRawReferences.pop();
- const input = wrapper.find({ ref: 'input' });
+ const input = wrapper.findComponent({ ref: 'input' });
input.element.value = newInputValue;
input.element.selectionStart = newInputValue.length;
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index cb044b9e891..649d8eef6ec 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -220,7 +220,7 @@ describe('Release edit/new component', () => {
});
it('renders a checkbox to include release notes', () => {
- expect(wrapper.find(GlFormCheckbox).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true);
});
});
@@ -238,7 +238,7 @@ describe('Release edit/new component', () => {
beforeEach(factory);
it('renders the asset links portion of the form', () => {
- expect(wrapper.find(AssetLinksForm).exists()).toBe(true);
+ expect(wrapper.findComponent(AssetLinksForm).exists()).toBe(true);
});
});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index c2ea6900d6e..9ca25b3b69a 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -36,8 +36,8 @@ describe('Release show component', () => {
wrapper = null;
});
- const findLoadingSkeleton = () => wrapper.find(ReleaseSkeletonLoader);
- const findReleaseBlock = () => wrapper.find(ReleaseBlock);
+ const findLoadingSkeleton = () => wrapper.findComponent(ReleaseSkeletonLoader);
+ const findReleaseBlock = () => wrapper.findComponent(ReleaseBlock);
const expectLoadingIndicator = () => {
it('renders a loading indicator', () => {
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 17f079ba5a6..1ff5766b074 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -73,7 +73,7 @@ describe('Release edit component', () => {
it('calls the "addEmptyAssetLink" store method when the "Add another link" button is clicked', () => {
expect(actions.addEmptyAssetLink).not.toHaveBeenCalled();
- wrapper.find({ ref: 'addAnotherLinkButton' }).vm.$emit('click');
+ wrapper.findComponent({ ref: 'addAnotherLinkButton' }).vm.$emit('click');
expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
});
@@ -92,7 +92,7 @@ describe('Release edit component', () => {
let newUrl;
beforeEach(() => {
- input = wrapper.find({ ref: 'urlInput' }).element;
+ input = wrapper.findComponent({ ref: 'urlInput' }).element;
linkIdToUpdate = release.assets.links[0].id;
newUrl = 'updated url';
});
@@ -118,7 +118,7 @@ describe('Release edit component', () => {
it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
expectStoreMethodNotToBeCalled();
- wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
+ wrapper.findComponent({ ref: 'urlInput' }).vm.$emit('change', newUrl);
expectStoreMethodToBeCalled();
});
@@ -150,7 +150,7 @@ describe('Release edit component', () => {
let newName;
beforeEach(() => {
- input = wrapper.find({ ref: 'nameInput' }).element;
+ input = wrapper.findComponent({ ref: 'nameInput' }).element;
linkIdToUpdate = release.assets.links[0].id;
newName = 'updated name';
});
@@ -176,7 +176,7 @@ describe('Release edit component', () => {
it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
expectStoreMethodNotToBeCalled();
- wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
+ wrapper.findComponent({ ref: 'nameInput' }).vm.$emit('change', newName);
expectStoreMethodToBeCalled();
});
@@ -208,7 +208,7 @@ describe('Release edit component', () => {
expect(actions.updateAssetLinkType).not.toHaveBeenCalled();
- wrapper.find({ ref: 'typeSelect' }).vm.$emit('change', newType);
+ wrapper.findComponent({ ref: 'typeSelect' }).vm.$emit('change', newType);
expect(actions.updateAssetLinkType).toHaveBeenCalledTimes(1);
expect(actions.updateAssetLinkType).toHaveBeenCalledWith(expect.anything(), {
@@ -225,7 +225,7 @@ describe('Release edit component', () => {
});
it('selects the default asset type', () => {
- const selected = wrapper.find({ ref: 'typeSelect' }).element.value;
+ const selected = wrapper.findComponent({ ref: 'typeSelect' }).element.value;
expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE);
});
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index f0d02884305..2db1e9e38a2 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -32,19 +32,19 @@ describe('Evidence Block', () => {
});
it('renders the evidence icon', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe('review-list');
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('review-list');
});
it('renders the title for the dowload link', () => {
- expect(wrapper.find(GlLink).text()).toBe(`v1.1-evidences-1.json`);
+ expect(wrapper.findComponent(GlLink).text()).toBe(`v1.1-evidences-1.json`);
});
it('renders the correct hover text for the download', () => {
- expect(wrapper.find(GlLink).attributes('title')).toBe('Download evidence JSON');
+ expect(wrapper.findComponent(GlLink).attributes('title')).toBe('Download evidence JSON');
});
it('renders the correct file link for download', () => {
- expect(wrapper.find(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`);
+ expect(wrapper.findComponent(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`);
});
describe('sha text', () => {
@@ -62,15 +62,15 @@ describe('Evidence Block', () => {
describe('copy to clipboard button', () => {
it('renders button', () => {
- expect(wrapper.find(ClipboardButton).exists()).toBe(true);
+ expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true);
});
it('renders the correct hover text', () => {
- expect(wrapper.find(ClipboardButton).attributes('title')).toBe('Copy evidence SHA');
+ expect(wrapper.findComponent(ClipboardButton).attributes('title')).toBe('Copy evidence SHA');
});
it('copies the sha', () => {
- expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe(
+ expect(wrapper.findComponent(ClipboardButton).attributes('data-clipboard-text')).toBe(
release.evidences[0].sha,
);
});
diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js
index 8fc0779da14..3ac75e138ee 100644
--- a/spec/frontend/releases/components/issuable_stats_spec.js
+++ b/spec/frontend/releases/components/issuable_stats_spec.js
@@ -16,9 +16,11 @@ describe('~/releases/components/issuable_stats.vue', () => {
});
};
- const findOpenStatLink = () => wrapper.find('[data-testid="open-stat"]').find(GlLink);
- const findMergedStatLink = () => wrapper.find('[data-testid="merged-stat"]').find(GlLink);
- const findClosedStatLink = () => wrapper.find('[data-testid="closed-stat"]').find(GlLink);
+ const findOpenStatLink = () => wrapper.find('[data-testid="open-stat"]').findComponent(GlLink);
+ const findMergedStatLink = () =>
+ wrapper.find('[data-testid="merged-stat"]').findComponent(GlLink);
+ const findClosedStatLink = () =>
+ wrapper.find('[data-testid="closed-stat"]').findComponent(GlLink);
beforeEach(() => {
defaultProps = {
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index c63689e11ac..4f94e4dfd55 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -44,7 +44,7 @@ describe('Release block assets', () => {
});
it('renders the accordion as expanded by default', () => {
- const accordion = wrapper.find(GlCollapse);
+ const accordion = wrapper.findComponent(GlCollapse);
expect(accordion.exists()).toBe(true);
expect(accordion.isVisible()).toBe(true);
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 848e802df4b..8f4efad197f 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -38,16 +38,16 @@ describe('Release block footer', () => {
});
const commitInfoSection = () => wrapper.find('.js-commit-info');
- const commitInfoSectionLink = () => commitInfoSection().find(GlLink);
+ const commitInfoSectionLink = () => commitInfoSection().findComponent(GlLink);
const tagInfoSection = () => wrapper.find('.js-tag-info');
- const tagInfoSectionLink = () => tagInfoSection().find(GlLink);
+ const tagInfoSectionLink = () => tagInfoSection().findComponent(GlLink);
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
describe('with all props provided', () => {
beforeEach(() => factory());
it('renders the commit icon', () => {
- const commitIcon = commitInfoSection().find(GlIcon);
+ const commitIcon = commitInfoSection().findComponent(GlIcon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('commit');
@@ -62,14 +62,14 @@ describe('Release block footer', () => {
});
it('renders the tag icon', () => {
- const commitIcon = tagInfoSection().find(GlIcon);
+ const commitIcon = tagInfoSection().findComponent(GlIcon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('tag');
});
it('renders the tag name with a link', () => {
- const commitLink = tagInfoSection().find(GlLink);
+ const commitLink = tagInfoSection().findComponent(GlLink);
expect(commitLink.exists()).toBe(true);
expect(commitLink.text()).toBe(release.tagName);
@@ -120,7 +120,7 @@ describe('Release block footer', () => {
});
it("renders a link to the author's profile", () => {
- const authorLink = authorDateInfoSection().find(GlLink);
+ const authorLink = authorDateInfoSection().findComponent(GlLink);
expect(authorLink.exists()).toBe(true);
expect(authorLink.attributes('href')).toBe(release.author.webUrl);
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index c9921185bad..fc421776d60 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -30,7 +30,7 @@ describe('Release block header', () => {
});
const findHeader = () => wrapper.find('h2');
- const findHeaderLink = () => findHeader().find(GlLink);
+ const findHeaderLink = () => findHeader().findComponent(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
const findBadge = () => wrapper.findComponent(GlBadge);
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index 84a0080965b..541d487091c 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -43,7 +43,7 @@ describe('Release block milestone info', () => {
});
it('renders a progress bar that displays the correct percentage', () => {
- const progressBar = milestoneProgressBarContainer().find(GlProgressBar);
+ const progressBar = milestoneProgressBarContainer().findComponent(GlProgressBar);
expect(progressBar.exists()).toBe(true);
expect(progressBar.attributes()).toEqual(
@@ -58,7 +58,7 @@ describe('Release block milestone info', () => {
expect(milestoneListContainer().text()).toMatchInterpolatedText('Milestones 12.3 • 12.4');
milestones.forEach((m, i) => {
- const milestoneLink = milestoneListContainer().findAll(GlLink).at(i);
+ const milestoneLink = milestoneListContainer().findAllComponents(GlLink).at(i);
expect(milestoneLink.text()).toBe(m.title);
expect(milestoneLink.attributes('href')).toBe(m.webUrl);
@@ -72,7 +72,7 @@ describe('Release block milestone info', () => {
expect(issuesContainerText).toContain(`Issues ${totalIssueCount}`);
- const badge = issuesContainer().find(GlBadge);
+ const badge = issuesContainer().findComponent(GlBadge);
expect(badge.text()).toBe(totalIssueCount.toString());
expect(issuesContainerText).toContain('Open: 5 • Closed: 4');
@@ -107,7 +107,7 @@ describe('Release block milestone info', () => {
});
const clickShowMoreFewerButton = async () => {
- milestoneListContainer().find(GlButton).trigger('click');
+ milestoneListContainer().findComponent(GlButton).trigger('click');
await nextTick();
};
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 17e2af687a6..096c3db8902 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -74,7 +74,7 @@ describe('Release block', () => {
});
it('renders the footer', () => {
- expect(wrapper.find(ReleaseBlockFooter).exists()).toBe(true);
+ expect(wrapper.findComponent(ReleaseBlockFooter).exists()).toBe(true);
});
});
@@ -133,7 +133,7 @@ describe('Release block', () => {
describe('evidence block', () => {
it('renders the evidence block when the evidence is available', () => {
return factory(release).then(() => {
- expect(wrapper.find(EvidenceBlock).exists()).toBe(true);
+ expect(wrapper.findComponent(EvidenceBlock).exists()).toBe(true);
});
});
@@ -141,7 +141,7 @@ describe('Release block', () => {
release.evidences = [];
return factory(release).then(() => {
- expect(wrapper.find(EvidenceBlock).exists()).toBe(false);
+ expect(wrapper.findComponent(EvidenceBlock).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/releases/components/release_skeleton_loader_spec.js b/spec/frontend/releases/components/release_skeleton_loader_spec.js
index 7f81081ff6c..76dfe0d9777 100644
--- a/spec/frontend/releases/components/release_skeleton_loader_spec.js
+++ b/spec/frontend/releases/components/release_skeleton_loader_spec.js
@@ -10,6 +10,6 @@ describe('release_skeleton_loader.vue', () => {
});
it('renders a GlSkeletonLoader', () => {
- expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index f45a28392b7..8105aa4f6f2 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -20,7 +20,7 @@ describe('releases/components/tag_field_existing', () => {
});
};
- const findInput = () => wrapper.find(GlFormInput);
+ const findInput = () => wrapper.findComponent(GlFormInput);
const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
beforeEach(() => {
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 9f500c318ea..b8047cae8c2 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -79,12 +79,12 @@ describe('releases/components/tag_field_new', () => {
});
const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
- const findTagNameDropdown = () => findTagNameFormGroup().find(RefSelectorStub);
+ const findTagNameDropdown = () => findTagNameFormGroup().findComponent(RefSelectorStub);
const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]');
- const findCreateFromDropdown = () => findCreateFromFormGroup().find(RefSelectorStub);
+ const findCreateFromDropdown = () => findCreateFromFormGroup().findComponent(RefSelectorStub);
- const findCreateNewTagOption = () => wrapper.find(GlDropdownItem);
+ const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem);
describe('"Tag name" field', () => {
describe('rendering and behavior', () => {
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
index e7b9aa4abbb..85a40f02c53 100644
--- a/spec/frontend/releases/components/tag_field_spec.js
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -21,8 +21,8 @@ describe('releases/components/tag_field', () => {
wrapper = shallowMount(TagField, { store });
};
- const findTagFieldNew = () => wrapper.find(TagFieldNew);
- const findTagFieldExisting = () => wrapper.find(TagFieldExisting);
+ const findTagFieldNew = () => wrapper.findComponent(TagFieldNew);
+ const findTagFieldExisting = () => wrapper.findComponent(TagFieldExisting);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index ce3b690213c..48fba3adb24 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -352,6 +352,32 @@ describe('Release edit/new actions', () => {
});
});
+ describe('when the GraphQL returns errors as data', () => {
+ beforeEach(() => {
+ gqClient.mutate.mockResolvedValue({ data: { releaseCreate: { errors: ['Yikes!'] } } });
+ });
+
+ it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
+ return testAction(actions.createRelease, undefined, state, [
+ {
+ type: types.RECEIVE_SAVE_RELEASE_ERROR,
+ payload: expect.any(Error),
+ },
+ ]);
+ });
+
+ it(`shows a flash message`, () => {
+ return actions
+ .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
+ .then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Yikes!',
+ });
+ });
+ });
+ });
+
describe('when the GraphQL network request fails', () => {
beforeEach(() => {
gqClient.mutate.mockRejectedValue(error);
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 4ac6eaebaa2..2982dc5c46c 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -320,7 +320,9 @@ describe('Release edit/new getters', () => {
it(description, () => {
const expectedVariablesObject = { input: expect.objectContaining(expectedVariables) };
- const actualVariables = getters.releaseUpdateMutatationVariables(state);
+ const actualVariables = getters.releaseUpdateMutatationVariables(state, {
+ releasedAtChanged: Object.hasOwn(state.release, 'releasedAt'),
+ });
expect(actualVariables).toEqual(expectedVariablesObject);
});
@@ -409,4 +411,19 @@ describe('Release edit/new getters', () => {
},
);
});
+
+ describe('releasedAtChange', () => {
+ it('is false if the released at date has not changed', () => {
+ const date = new Date();
+ expect(
+ getters.releasedAtChanged({ originalReleasedAt: date, release: { releasedAt: date } }),
+ ).toBe(false);
+ });
+
+ it('is true if the date changed', () => {
+ const originalReleasedAt = new Date();
+ const releasedAt = new Date(2022, 5, 30);
+ expect(getters.releasedAtChanged({ originalReleasedAt, release: { releasedAt } })).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 60b57c7a7ff..8bbf550b77d 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -36,6 +36,12 @@ describe('Release edit/new mutations', () => {
},
});
});
+
+ it('saves the original released at date as well', () => {
+ mutations[types.INITIALIZE_EMPTY_RELEASE](state);
+
+ expect(state.originalReleasedAt).toEqual(new Date());
+ });
});
describe(`${types.REQUEST_RELEASE}`, () => {
@@ -57,6 +63,7 @@ describe('Release edit/new mutations', () => {
expect(state.release).toEqual(release);
expect(state.originalRelease).toEqual(release);
+ expect(state.originalReleasedAt).toEqual(release.releasedAt);
});
});
diff --git a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
index ddabb7194cb..d835ca4c733 100644
--- a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
+++ b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
@@ -41,7 +41,7 @@ describe('CustomMetricsForm', () => {
});
it('Displays the issue message', () => {
- const description = wrapper.find({ ref: 'accessibility-issue-description' }).text();
+ const description = wrapper.findComponent({ ref: 'accessibility-issue-description' }).text();
expect(description).toContain(`Message: ${issue.message}`);
});
@@ -49,7 +49,7 @@ describe('CustomMetricsForm', () => {
describe('When an issue code is present', () => {
it('Creates the correct URL for learning more about the issue code', () => {
const learnMoreUrl = wrapper
- .find({ ref: 'accessibility-issue-learn-more' })
+ .findComponent({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
expect(learnMoreUrl).toBe(issue.learnMoreUrl);
@@ -66,7 +66,7 @@ describe('CustomMetricsForm', () => {
it('Creates a URL leading to the overview documentation page', () => {
const learnMoreUrl = wrapper
- .find({ ref: 'accessibility-issue-learn-more' })
+ .findComponent({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
@@ -83,7 +83,7 @@ describe('CustomMetricsForm', () => {
it('Creates a URL leading to the overview documentation page', () => {
const learnMoreUrl = wrapper
- .find({ ref: 'accessibility-issue-learn-more' })
+ .findComponent({ ref: 'accessibility-issue-learn-more' })
.attributes('href');
expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
index 34b1cdd92bc..9d3535291eb 100644
--- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
+++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
@@ -114,7 +114,7 @@ describe('Grouped accessibility reports app', () => {
});
it('renders custom accessibility issue body', () => {
- const issueBody = wrapper.find(AccessibilityIssueBody);
+ const issueBody = wrapper.findComponent(AccessibilityIssueBody);
expect(issueBody.props('issue').code).toBe(mockReport.new_errors[0].code);
expect(issueBody.props('issue').message).toBe(mockReport.new_errors[0].message);
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
index 17f07ac2b8f..c32b52d9e77 100644
--- a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -8,7 +8,7 @@ describe('code quality issue body issue body', () => {
let wrapper;
const findSeverityIcon = () => wrapper.findByTestId('codequality-severity-icon');
- const findGlIcon = () => wrapper.find(GlIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
const codequalityIssue = {
name:
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
index b61b65c2713..962ff068b92 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -30,7 +30,7 @@ describe('Grouped code quality reports app', () => {
};
const findWidget = () => wrapper.find('.js-codequality-widget');
- const findIssueBody = () => wrapper.find(CodequalityIssueBody);
+ const findIssueBody = () => wrapper.findComponent(CodequalityIssueBody);
beforeEach(() => {
const { state, ...storeConfig } = getStoreConfig();
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
index 95ef0bcbcc7..6c0275dc47d 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -30,7 +30,7 @@ describe('Grouped Issues List', () => {
},
});
- expect(wrapper.find(SmartVirtualList).props()).toMatchSnapshot();
+ expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot();
});
describe('without data', () => {
@@ -43,7 +43,7 @@ describe('Grouped Issues List', () => {
});
it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => {
- expect(wrapper.find(ReportItem).exists()).toBe(false);
+ expect(wrapper.findComponent(ReportItem).exists()).toBe(false);
});
});
@@ -67,7 +67,7 @@ describe('Grouped Issues List', () => {
propsData: { [`${issueName}Issues`]: issues },
});
- expect(wrapper.findAll(ReportItem)).toHaveLength(issues.length);
+ expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length);
});
it('renders a report item with the correct props', () => {
@@ -81,7 +81,7 @@ describe('Grouped Issues List', () => {
},
});
- expect(wrapper.find(ReportItem).props()).toMatchSnapshot();
+ expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot();
});
});
});
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
index a7243c5377b..b52c163eb26 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -16,7 +16,7 @@ describe('ReportItem', () => {
},
});
- expect(wrapper.find(IssueStatusIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(IssueStatusIcon).exists()).toBe(false);
});
it('shows status icon when unspecified', () => {
@@ -28,7 +28,7 @@ describe('ReportItem', () => {
},
});
- expect(wrapper.find(IssueStatusIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(IssueStatusIcon).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/reports/grouped_test_report/components/modal_spec.js b/spec/frontend/reports/grouped_test_report/components/modal_spec.js
index 3de81f754fd..e8564d2428d 100644
--- a/spec/frontend/reports/grouped_test_report/components/modal_spec.js
+++ b/spec/frontend/reports/grouped_test_report/components/modal_spec.js
@@ -40,7 +40,9 @@ describe('Grouped Test Reports Modal', () => {
});
it('renders code block', () => {
- expect(wrapper.find(CodeBlock).props().code).toEqual(modalDataStructure.system_output.value);
+ expect(wrapper.findComponent(CodeBlock).props().code).toEqual(
+ modalDataStructure.system_output.value,
+ );
});
it('renders link', () => {
diff --git a/spec/frontend/reports/grouped_test_report/store/actions_spec.js b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
index 5876827c548..7469c31cf84 100644
--- a/spec/frontend/reports/grouped_test_report/store/actions_spec.js
+++ b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
@@ -61,7 +61,7 @@ describe('Reports Store Actions', () => {
});
describe('success', () => {
- it('dispatches requestReports and receiveReportsSuccess ', () => {
+ it('dispatches requestReports and receiveReportsSuccess', () => {
mock
.onGet(`${TEST_HOST}/endpoint.json`)
.replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
@@ -89,7 +89,7 @@ describe('Reports Store Actions', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
- it('dispatches requestReports and receiveReportsError ', () => {
+ it('dispatches requestReports and receiveReportsError', () => {
return testAction(
fetchReports,
null,
diff --git a/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json b/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json
new file mode 100644
index 00000000000..28ee7d194b9
--- /dev/null
+++ b/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json
@@ -0,0 +1,40 @@
+{
+ "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 },
+ "suites": [
+ {
+ "name": "rspec:pg",
+ "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 },
+ "new_failures": [
+ {
+ "result": "failure",
+ "name": "Test#sum when a is 1 and b is 2 returns summary",
+ "file": null,
+ "execution_time": 0.009411,
+ "system_output": "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'"
+ },
+ {
+ "result": "failure",
+ "name": "Test#sum when a is 100 and b is 200 returns summary",
+ "file": null,
+ "execution_time": 0.000162,
+ "system_output": "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'"
+ }
+ ],
+ "resolved_failures": [],
+ "existing_failures": [],
+ "new_errors": [],
+ "resolved_errors": [],
+ "existing_errors": []
+ },
+ {
+ "name": "java ant",
+ "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "new_failures": [],
+ "resolved_failures": [],
+ "existing_failures": [],
+ "new_errors": [],
+ "resolved_errors": [],
+ "existing_errors": []
+ }
+ ]
+}
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index cb56f392ec9..01494cb6a24 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -20,7 +20,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-flex-grow-1 gl-min-w-0"
>
<div
- class="commit-content qa-commit-content"
+ class="commit-content"
+ data-qa-selector="commit_content"
>
<gl-link-stub
class="commit-row-message item-title"
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index d5b882bd715..33a85c04fcf 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -66,12 +66,12 @@ describe('BlobButtonGroup component', () => {
});
it('renders both the replace and delete button', () => {
- expect(wrapper.findAll(GlButton)).toHaveLength(2);
+ expect(wrapper.findAllComponents(GlButton)).toHaveLength(2);
});
it('renders the buttons in the correct order', () => {
- expect(wrapper.findAll(GlButton).at(0).text()).toBe('Replace');
- expect(wrapper.findAll(GlButton).at(1).text()).toBe('Delete');
+ expect(wrapper.findAllComponents(GlButton).at(0).text()).toBe('Replace');
+ expect(wrapper.findAllComponents(GlButton).at(1).text()).toBe('Delete');
});
it('triggers the UploadBlobModal from the replace button', () => {
@@ -97,14 +97,14 @@ describe('BlobButtonGroup component', () => {
findReplaceButton().trigger('click');
expect(findUploadBlobModal().vm.show).not.toHaveBeenCalled();
- expect(wrapper.emitted().fork).toBeTruthy();
+ expect(wrapper.emitted().fork).toHaveLength(1);
});
it('does not trigger the DeleteBlobModal from the delete button', () => {
findDeleteButton().trigger('click');
expect(findDeleteBlobModal().vm.show).not.toHaveBeenCalled();
- expect(wrapper.emitted().fork).toBeTruthy();
+ expect(wrapper.emitted().fork).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 0f7cf4e61b2..6ece72c41bb 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -17,7 +17,8 @@ import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
-import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
+import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
+import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import userInfoQuery from '~/repository/queries/user_info.query.graphql';
import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
@@ -45,8 +46,9 @@ jest.mock('~/lib/utils/common_utils');
jest.mock('~/blob/line_highlighter');
let wrapper;
-let mockResolver;
+let blobInfoMockResolver;
let userInfoMockResolver;
+let projectInfoMockResolver;
let applicationInfoMockResolver;
const mockAxios = new MockAdapter(axios);
@@ -74,22 +76,40 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
highlightJs = true,
} = mockData;
- const project = {
+ const blobInfo = {
...projectMock,
+ repository: {
+ empty,
+ blobs: { nodes: [blob] },
+ },
+ };
+
+ const projectInfo = {
+ __typename: 'Project',
+ id: '123',
userPermissions: {
pushCode,
forkProject,
downloadCode,
createMergeRequestIn,
},
- repository: {
- empty,
- blobs: { nodes: [blob] },
+ pathLocks: {
+ nodes: [
+ {
+ id: 'test',
+ path: 'locked_file.js',
+ user: { id: '123', username: 'root' },
+ },
+ ],
},
};
- mockResolver = jest.fn().mockResolvedValue({
- data: { isBinary, project },
+ projectInfoMockResolver = jest.fn().mockResolvedValue({
+ data: { project: projectInfo },
+ });
+
+ blobInfoMockResolver = jest.fn().mockResolvedValue({
+ data: { isBinary, project: blobInfo },
});
userInfoMockResolver = jest.fn().mockResolvedValue({
@@ -101,8 +121,9 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
});
const fakeApollo = createMockApollo([
- [blobInfoQuery, mockResolver],
+ [blobInfoQuery, blobInfoMockResolver],
[userInfoQuery, userInfoMockResolver],
+ [projectInfoQuery, projectInfoMockResolver],
[applicationInfoQuery, applicationInfoMockResolver],
]);
@@ -129,7 +150,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ project, isBinary });
+ wrapper.setData({ project: blobInfo, isBinary });
await waitForPromises();
};
@@ -504,14 +525,16 @@ describe('Blob content viewer component', () => {
async ({ highlightJs, shouldFetchRawText }) => {
await createComponent({ highlightJs });
- expect(mockResolver).toHaveBeenCalledWith(expect.objectContaining({ shouldFetchRawText }));
+ expect(blobInfoMockResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ shouldFetchRawText }),
+ );
},
);
it('is called with originalBranch value if the prop has a value', async () => {
await createComponent({ inject: { originalBranch: 'some-branch' } });
- expect(mockResolver).toHaveBeenCalledWith(
+ expect(blobInfoMockResolver).toHaveBeenCalledWith(
expect.objectContaining({
ref: 'some-branch',
}),
@@ -521,7 +544,7 @@ describe('Blob content viewer component', () => {
it('is called with ref value if the originalBranch prop has no value', async () => {
await createComponent();
- expect(mockResolver).toHaveBeenCalledWith(
+ expect(blobInfoMockResolver).toHaveBeenCalledWith(
expect.objectContaining({
ref: 'default-ref',
}),
diff --git a/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js
index 7d43e4e660b..c6b9737dde2 100644
--- a/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js
@@ -21,7 +21,7 @@ describe('CSV Viewer', () => {
it('renders a Source Editor component', () => {
createComponent();
expect(findCsvViewerComp().exists()).toBe(true);
- expect(findCsvViewerComp().props('remoteFile')).toBeTruthy();
+ expect(findCsvViewerComp().props('remoteFile')).toBe(true);
expect(findCsvViewerComp().props('csv')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 40b32904589..c2f34f79f89 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -39,8 +39,8 @@ describe('Repository breadcrumbs component', () => {
});
};
- const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
- const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal);
+ const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
+ const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
afterEach(() => {
wrapper.destroy();
@@ -55,7 +55,7 @@ describe('Repository breadcrumbs component', () => {
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory(path);
- expect(wrapper.findAll(RouterLinkStub).length).toEqual(linkCount);
+ expect(wrapper.findAllComponents(RouterLinkStub).length).toEqual(linkCount);
});
it.each`
@@ -68,14 +68,14 @@ describe('Repository breadcrumbs component', () => {
'links to the correct router path when routeName is $routeName',
({ routeName, path, linkTo }) => {
factory(path, {}, { name: routeName });
- expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual(linkTo);
+ expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(linkTo);
},
);
it('escapes hash in directory path', () => {
factory('app/assets/javascripts#');
- expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual(
+ expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(
'/-/tree/app/assets/javascripts%23',
);
});
@@ -83,7 +83,9 @@ describe('Repository breadcrumbs component', () => {
it('renders last link as active', () => {
factory('app/assets');
- expect(wrapper.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page');
+ expect(wrapper.findAllComponents(RouterLinkStub).at(2).attributes('aria-current')).toEqual(
+ 'page',
+ );
});
it('does not render add to tree dropdown when permissions are false', async () => {
@@ -95,7 +97,7 @@ describe('Repository breadcrumbs component', () => {
await nextTick();
- expect(wrapper.find(GlDropdown).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});
it.each`
@@ -109,7 +111,7 @@ describe('Repository breadcrumbs component', () => {
'does render add to tree dropdown $isRendered when route is $routeName',
({ routeName, isRendered }) => {
factory('app/assets/javascripts.js', { canCollaborate: true }, { name: routeName });
- expect(wrapper.find(GlDropdown).exists()).toBe(isRendered);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(isRendered);
},
);
@@ -122,7 +124,7 @@ describe('Repository breadcrumbs component', () => {
await nextTick();
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
describe('renders the upload blob modal', () => {
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
index 785783b2e75..b5996816ad8 100644
--- a/spec/frontend/repository/components/delete_blob_modal_spec.js
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -84,7 +84,7 @@ describe('DeleteBlobModal', () => {
${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false}
`(
- 'has the correct form fields ',
+ 'has the correct form fields',
({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
createComponent({
canPushCode,
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 3783b34e33a..bf9528953b6 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -190,11 +190,16 @@ describe('Repository last commit component', () => {
});
it('expands commit description when clicking expander', async () => {
+ expect(findCommitRowDescription().classes('d-block')).toBe(false);
+ expect(findTextExpander().classes('open')).toBe(false);
+ expect(findTextExpander().props('selected')).toBe(false);
+
findTextExpander().vm.$emit('click');
await nextTick();
- expect(findCommitRowDescription().isVisible()).toBe(true);
- expect(findTextExpander().classes()).toContain('open');
+ expect(findCommitRowDescription().classes('d-block')).toBe(true);
+ expect(findTextExpander().classes('open')).toBe(true);
+ expect(findTextExpander().props('selected')).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index e1c50d63851..aaf751a9a8d 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -107,7 +107,7 @@ describe('NewDirectoryModal', () => {
${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true}
${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true}
`(
- 'has the correct form fields ',
+ 'has the correct form fields',
({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
createComponent({
canPushCode,
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index 0d9bfc62ed5..e4eba65795e 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -68,6 +68,6 @@ describe('Repository file preview component', () => {
vm.setData({ loading: 1 });
await nextTick();
- expect(vm.find(GlLoadingIcon).exists()).toBe(true);
+ expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index ff0371b5c07..697d2dcc7f5 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -38,6 +38,7 @@ const MOCK_BLOBS = [
const MOCK_COMMITS = [
{
fileName: 'blob.md',
+ filePath: 'test_dir/blob.md',
type: 'blob',
commit: {
message: 'Updated blob.md',
@@ -45,6 +46,7 @@ const MOCK_COMMITS = [
},
{
fileName: 'blob2.md',
+ filePath: 'test_dir/blob2.md',
type: 'blob',
commit: {
message: 'Updated blob2.md',
@@ -52,11 +54,20 @@ const MOCK_COMMITS = [
},
{
fileName: 'blob3.md',
+ filePath: 'test_dir/blob3.md',
type: 'blob',
commit: {
message: 'Updated blob3.md',
},
},
+ {
+ fileName: 'root_blob.md',
+ filePath: '/root_blob.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated root_blob.md',
+ },
+ },
];
function factory({ path, isLoading = false, hasMore = true, entries = {}, commits = [] }) {
@@ -77,6 +88,8 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit
});
}
+const findTableRows = () => vm.findAllComponents(TableRow);
+
describe('Repository table component', () => {
afterEach(() => {
vm.destroy();
@@ -108,14 +121,14 @@ describe('Repository table component', () => {
it('renders table rows', () => {
factory({
- path: '/',
+ path: 'test_dir',
entries: {
blobs: MOCK_BLOBS,
},
commits: MOCK_COMMITS,
});
- const rows = vm.findAll(TableRow);
+ const rows = findTableRows();
expect(rows.length).toEqual(3);
expect(rows.at(2).attributes().mode).toEqual('120000');
@@ -123,6 +136,28 @@ describe('Repository table component', () => {
expect(rows.at(2).props().commitInfo).toEqual(MOCK_COMMITS[2]);
});
+ it('renders correct commit info for blobs in the root', () => {
+ factory({
+ path: '/',
+ entries: {
+ blobs: [
+ {
+ id: '126abc',
+ sha: '126abc',
+ flatPath: 'root_blob.md',
+ name: 'root_blob.md',
+ type: 'blob',
+ webUrl: 'http://test.com',
+ mode: '120000',
+ },
+ ],
+ },
+ commits: MOCK_COMMITS,
+ });
+
+ expect(findTableRows().at(0).props().commitInfo).toEqual(MOCK_COMMITS[3]);
+ });
+
describe('Show more button', () => {
const showMoreButton = () => vm.find(GlButton);
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index bf024baa627..505ff7f3dd6 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -217,7 +217,7 @@ describe('UploadBlobModal', () => {
createComponent();
});
- it('displays the default "Upload new file" modal title ', () => {
+ it('displays the default "Upload new file" modal title', () => {
expect(findModal().props('title')).toBe('Upload new file');
});
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index e3b4dcb8acc..c1309539b6d 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -30,7 +30,7 @@ describe('resolveCommit', () => {
{ fileName: 'index.js', filePath: '/app/assets/index.js' },
];
- resolveCommit(commits, '', resolver);
+ resolveCommit(commits, '/', resolver);
expect(resolver.resolve).toHaveBeenCalledWith({
fileName: 'index.js',
@@ -107,14 +107,14 @@ describe('fetchLogsTree', () => {
}));
it('calls entry resolver', () =>
- fetchLogsTree(client, '', '0', resolver).then(() => {
+ fetchLogsTree(client, 'test', '0', resolver).then(() => {
expect(resolver.resolve).toHaveBeenCalledWith(
expect.objectContaining({
__typename: 'LogTreeCommit',
commitPath: 'https://test.com',
committedDate: '2019-01-01',
fileName: 'index.js',
- filePath: '/index.js',
+ filePath: 'test/index.js',
message: 'testing message',
sha: '123',
}),
@@ -122,7 +122,7 @@ describe('fetchLogsTree', () => {
}));
it('writes query to client', async () => {
- await fetchLogsTree(client, '', '0', resolver);
+ await fetchLogsTree(client, '/', '0', resolver);
expect(client.readQuery({ query: commitsQuery })).toEqual({
commits: [
expect.objectContaining({
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 4db295fe0b7..cda47a5b0a5 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -1,4 +1,5 @@
export const simpleViewerMock = {
+ __typename: 'RepositoryBlob',
id: '1',
name: 'some_file.js',
size: 123,
diff --git a/spec/frontend/repository/utils/commit_spec.js b/spec/frontend/repository/utils/commit_spec.js
index b3dd5118308..65728e9cb24 100644
--- a/spec/frontend/repository/utils/commit_spec.js
+++ b/spec/frontend/repository/utils/commit_spec.js
@@ -15,7 +15,7 @@ const mockData = [
describe('normalizeData', () => {
it('normalizes data into LogTreeCommit object', () => {
- expect(normalizeData(mockData, '')).toEqual([
+ expect(normalizeData(mockData, '/')).toEqual([
{
sha: '123',
message: 'testing message',
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index 509681c5a77..7ab4aeee9bc 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -164,7 +164,7 @@ describe('AdminRunnerShowApp', () => {
});
});
- describe('when runner does not have an edit url ', () => {
+ describe('when runner does not have an edit url', () => {
beforeEach(async () => {
mockRunnerQueryResult({
editAdminUrl: null,
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 97341be7d5d..55a298e1695 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -17,6 +17,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
import { createLocalState } from '~/runner/graphql/list/local_state';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
+import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
@@ -33,6 +34,12 @@ import {
CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT,
+ I18N_STATUS_ONLINE,
+ I18N_STATUS_OFFLINE,
+ I18N_STATUS_STALE,
+ I18N_INSTANCE_TYPE,
+ I18N_GROUP_TYPE,
+ I18N_PROJECT_TYPE,
INSTANCE_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
@@ -80,6 +87,7 @@ describe('AdminRunnersApp', () => {
let localMutations;
let showToast;
+ const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
@@ -139,6 +147,11 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows the feedback banner', () => {
+ createComponent();
+ expect(findRunnerStackedLayoutBanner().exists()).toBe(true);
+ });
+
it('shows the runner setup instructions', () => {
createComponent();
@@ -156,21 +169,16 @@ describe('AdminRunnersApp', () => {
});
it('shows the runner tabs', () => {
- expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
- `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`,
+ const tabs = findRunnerTypeTabs().text();
+ expect(tabs).toMatchInterpolatedText(
+ `All ${mockRunnersCount} ${I18N_INSTANCE_TYPE} ${mockRunnersCount} ${I18N_GROUP_TYPE} ${mockRunnersCount} ${I18N_PROJECT_TYPE} ${mockRunnersCount}`,
);
});
it('shows the total', () => {
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Online runners')} ${mockRunnersCount}`,
- );
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Offline runners')} ${mockRunnersCount}`,
- );
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Stale runners')} ${mockRunnersCount}`,
- );
+ expect(findRunnerStats().text()).toContain(`${I18N_STATUS_ONLINE} ${mockRunnersCount}`);
+ expect(findRunnerStats().text()).toContain(`${I18N_STATUS_OFFLINE} ${mockRunnersCount}`);
+ expect(findRunnerStats().text()).toContain(`${I18N_STATUS_STALE} ${mockRunnersCount}`);
});
});
diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
new file mode 100644
index 00000000000..21ec9f61f37
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
@@ -0,0 +1,164 @@
+import { __ } from '~/locale';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerStackedSummaryCell from '~/runner/components/cells/runner_stacked_summary_cell.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerTags from '~/runner/components/runner_tags.vue';
+import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
+
+import { allRunnersData } from '../../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+
+ const findLockIcon = () => wrapper.findByTestId('lock-icon');
+ const findRunnerTags = () => wrapper.findComponent(RunnerTags);
+ const findRunnerSummaryField = (icon) =>
+ wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
+ .wrappers[0];
+
+ const createComponent = (runner, options) => {
+ wrapper = mountExtended(RunnerStackedSummaryCell, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ },
+ stubs: {
+ RunnerSummaryField,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays the runner name as id and short token', () => {
+ expect(wrapper.text()).toContain(
+ `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
+ );
+ });
+
+ it('Does not display the locked icon', () => {
+ expect(findLockIcon().exists()).toBe(false);
+ });
+
+ it('Displays the locked icon for locked runners', () => {
+ createComponent({
+ runnerType: PROJECT_TYPE,
+ locked: true,
+ });
+
+ expect(findLockIcon().exists()).toBe(true);
+ });
+
+ it('Displays the runner type', () => {
+ createComponent({
+ runnerType: INSTANCE_TYPE,
+ locked: true,
+ });
+
+ expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE);
+ });
+
+ it('Displays the runner version', () => {
+ expect(wrapper.text()).toContain(mockRunner.version);
+ });
+
+ it('Displays the runner description', () => {
+ expect(wrapper.text()).toContain(mockRunner.description);
+ });
+
+ it('Displays last contact', () => {
+ createComponent({
+ contactedAt: '2022-01-02',
+ });
+
+ expect(findRunnerSummaryField('clock').find(TimeAgo).props('time')).toBe('2022-01-02');
+ });
+
+ it('Displays empty last contact', () => {
+ createComponent({
+ contactedAt: null,
+ });
+
+ expect(findRunnerSummaryField('clock').find(TimeAgo).exists()).toBe(false);
+ expect(findRunnerSummaryField('clock').text()).toContain(__('Never'));
+ });
+
+ it('Displays ip address', () => {
+ createComponent({
+ ipAddress: '127.0.0.1',
+ });
+
+ expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1');
+ });
+
+ it('Displays no ip address', () => {
+ createComponent({
+ ipAddress: null,
+ });
+
+ expect(findRunnerSummaryField('disk')).toBeUndefined();
+ });
+
+ it('Displays job count', () => {
+ expect(findRunnerSummaryField('pipeline').text()).toContain(`${mockRunner.jobCount}`);
+ });
+
+ it('Formats large job counts', () => {
+ createComponent({
+ jobCount: 1000,
+ });
+
+ expect(findRunnerSummaryField('pipeline').text()).toContain('1,000');
+ });
+
+ it('Formats large job counts with a plus symbol', () => {
+ createComponent({
+ jobCount: 1001,
+ });
+
+ expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
+ });
+
+ it('Displays created at', () => {
+ expect(findRunnerSummaryField('calendar').find(TimeAgo).props('time')).toBe(
+ mockRunner.createdAt,
+ );
+ });
+
+ it('Displays tag list', () => {
+ createComponent({
+ tagList: ['shell', 'linux'],
+ });
+
+ expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
+ });
+
+ it('Displays a custom slot', () => {
+ const slotContent = 'My custom runner name';
+
+ createComponent(
+ {},
+ {
+ slots: {
+ 'runner-name': slotContent,
+ },
+ },
+ );
+
+ expect(wrapper.text()).toContain(slotContent);
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
index 0f5133d0ae2..1d4e3762c91 100644
--- a/spec/frontend/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
@@ -3,7 +3,14 @@ import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
import RunnerPausedBadge from '~/runner/components/runner_paused_badge.vue';
-import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
+import {
+ I18N_PAUSED,
+ I18N_STATUS_ONLINE,
+ I18N_STATUS_OFFLINE,
+ INSTANCE_TYPE,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+} from '~/runner/constants';
describe('RunnerStatusCell', () => {
let wrapper;
@@ -31,8 +38,8 @@ describe('RunnerStatusCell', () => {
it('Displays online status', () => {
createComponent();
- expect(wrapper.text()).toMatchInterpolatedText('online');
- expect(findStatusBadge().text()).toBe('online');
+ expect(wrapper.text()).toContain(I18N_STATUS_ONLINE);
+ expect(findStatusBadge().text()).toBe(I18N_STATUS_ONLINE);
});
it('Displays offline status', () => {
@@ -42,8 +49,8 @@ describe('RunnerStatusCell', () => {
},
});
- expect(wrapper.text()).toMatchInterpolatedText('offline');
- expect(findStatusBadge().text()).toBe('offline');
+ expect(wrapper.text()).toMatchInterpolatedText(I18N_STATUS_OFFLINE);
+ expect(findStatusBadge().text()).toBe(I18N_STATUS_OFFLINE);
});
it('Displays paused status', () => {
@@ -54,8 +61,8 @@ describe('RunnerStatusCell', () => {
},
});
- expect(wrapper.text()).toMatchInterpolatedText('online paused');
- expect(findPausedBadge().text()).toBe('paused');
+ expect(wrapper.text()).toMatchInterpolatedText(`${I18N_STATUS_ONLINE} ${I18N_PAUSED}`);
+ expect(findPausedBadge().text()).toBe(I18N_PAUSED);
});
it('Is empty when data is missing', () => {
diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
deleted file mode 100644
index b06ab652212..00000000000
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { __ } from '~/locale';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
-import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
-
-const mockId = '1';
-const mockShortSha = '2P6oDVDm';
-const mockDescription = 'runner-1';
-const mockIpAddress = '0.0.0.0';
-
-describe('RunnerTypeCell', () => {
- let wrapper;
-
- const findLockIcon = () => wrapper.findByTestId('lock-icon');
-
- const createComponent = (runner, options) => {
- wrapper = mountExtended(RunnerSummaryCell, {
- propsData: {
- runner: {
- id: `gid://gitlab/Ci::Runner/${mockId}`,
- shortSha: mockShortSha,
- description: mockDescription,
- ipAddress: mockIpAddress,
- runnerType: INSTANCE_TYPE,
- ...runner,
- },
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays the runner name as id and short token', () => {
- expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
- });
-
- it('Displays the runner type', () => {
- expect(wrapper.text()).toContain('shared');
- });
-
- it('Does not display the locked icon', () => {
- expect(findLockIcon().exists()).toBe(false);
- });
-
- it('Displays the locked icon for locked runners', () => {
- createComponent({
- runnerType: PROJECT_TYPE,
- locked: true,
- });
-
- expect(findLockIcon().exists()).toBe(true);
- });
-
- it('Displays the runner description', () => {
- expect(wrapper.text()).toContain(mockDescription);
- });
-
- it('Displays ip address', () => {
- expect(wrapper.text()).toContain(`${__('IP Address')} ${mockIpAddress}`);
- });
-
- it('Displays no ip address', () => {
- createComponent({
- ipAddress: null,
- });
-
- expect(wrapper.text()).not.toContain(__('IP Address'));
- });
-
- it('Displays a custom slot', () => {
- const slotContent = 'My custom runner summary';
-
- createComponent(
- {},
- {
- slots: {
- 'runner-name': slotContent,
- },
- },
- );
-
- expect(wrapper.text()).toContain(slotContent);
- });
-});
diff --git a/spec/frontend/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/runner/components/cells/runner_summary_field_spec.js
new file mode 100644
index 00000000000..b49addf112f
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_summary_field_spec.js
@@ -0,0 +1,49 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+describe('RunnerSummaryField', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value;
+
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = shallowMount(RunnerSummaryField, {
+ propsData: {
+ icon: '',
+ tooltip: '',
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows content in slot', () => {
+ createComponent({
+ slots: { default: 'content' },
+ });
+
+ expect(wrapper.text()).toBe('content');
+ });
+
+ it('shows icon', () => {
+ createComponent({ props: { icon: 'git' } });
+
+ expect(findIcon().props('name')).toBe('git');
+ });
+
+ it('shows tooltip', () => {
+ createComponent({ props: { tooltip: 'tooltip' } });
+
+ expect(getTooltipValue()).toBe('tooltip');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index 552ee29b6f9..f2281223a25 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -25,7 +25,12 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
- const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({
+ props = {},
+ stubs,
+ mountFn = shallowMountExtended,
+ enforceRunnerTokenExpiresAt = false,
+ } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
...props,
@@ -34,6 +39,9 @@ describe('RunnerDetails', () => {
RunnerDetail,
...stubs,
},
+ provide: {
+ glFeatures: { enforceRunnerTokenExpiresAt },
+ },
});
};
@@ -63,6 +71,8 @@ describe('RunnerDetails', () => {
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
+ ${'Token expiry'} | ${{ tokenExpiresAt: mockOneHourAgo }} | ${'1 hour ago'}
+ ${'Token expiry'} | ${{ tokenExpiresAt: null }} | ${'Never expires'}
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
@@ -72,6 +82,7 @@ describe('RunnerDetails', () => {
...runner,
},
},
+ enforceRunnerTokenExpiresAt: true,
stubs: {
GlIntersperse,
GlSprintf,
@@ -124,5 +135,22 @@ describe('RunnerDetails', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
});
+
+ describe('Token expiration field', () => {
+ it.each`
+ case | flag | shown
+ ${'is shown when feature flag is enabled'} | ${true} | ${true}
+ ${'is not shown when feature flag is disabled'} | ${false} | ${false}
+ `('$case', ({ flag, shown }) => {
+ createComponent({
+ props: {
+ runner: mockGroupRunner,
+ },
+ enforceRunnerTokenExpiresAt: flag,
+ });
+
+ expect(findDd('Token expiry', wrapper).exists()).toBe(shown);
+ });
+ });
});
});
diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js
index 8799c218b06..701d39108cb 100644
--- a/spec/frontend/runner/components/runner_header_spec.js
+++ b/spec/frontend/runner/components/runner_header_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
+import { I18N_STATUS_ONLINE, I18N_GROUP_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -49,7 +49,7 @@ describe('RunnerHeader', () => {
},
});
- expect(findRunnerStatusBadge().text()).toContain('online');
+ expect(findRunnerStatusBadge().text()).toContain(I18N_STATUS_ONLINE);
});
it('displays the runner type', () => {
@@ -60,7 +60,7 @@ describe('RunnerHeader', () => {
},
});
- expect(findRunnerTypeBadge().text()).toContain('group');
+ expect(findRunnerTypeBadge().text()).toContain(I18N_GROUP_TYPE);
});
it('displays the runner id', () => {
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 7b58a81bb0d..54a9e713721 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -7,6 +7,7 @@ import {
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants';
import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
const mockRunners = allRunnersData.data.runners.nodes;
@@ -22,7 +23,10 @@ describe('RunnerList', () => {
const findCell = ({ row = 0, fieldKey }) =>
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
- const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => {
+ const createComponent = (
+ { props = {}, provide = {}, ...options } = {},
+ mountFn = shallowMountExtended,
+ ) => {
wrapper = mountFn(RunnerList, {
propsData: {
runners: mockRunners,
@@ -32,6 +36,7 @@ describe('RunnerList', () => {
provide: {
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ ...provide,
},
...options,
});
@@ -60,10 +65,6 @@ describe('RunnerList', () => {
expect(headerLabels).toEqual([
'Status',
'Runner',
- 'Version',
- 'Jobs',
- 'Tags',
- 'Last contact',
'', // actions has no label
]);
});
@@ -83,24 +84,28 @@ describe('RunnerList', () => {
});
it('Displays details of a runner', () => {
- const { id, description, version, shortSha } = mockRunners[0];
-
createComponent({}, mountExtended);
+ const { id, description, version, shortSha } = mockRunners[0];
+ const numericId = getIdFromGraphQLId(id);
+
// Badges
- expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('never contacted');
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText(
+ I18N_STATUS_NEVER_CONTACTED,
+ );
// Runner summary
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(
- `#${getIdFromGraphQLId(id)} (${shortSha})`,
- );
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(description);
+ const summary = findCell({ fieldKey: 'summary' }).text();
- // Other fields
- expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
- expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
- expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
+ expect(summary).toContain(`#${numericId} (${shortSha})`);
+ expect(summary).toContain(I18N_PROJECT_TYPE);
+
+ expect(summary).toContain(version);
+ expect(summary).toContain(description);
+
+ expect(summary).toContain('Last contact');
+ expect(summary).toContain('0'); // job count
+ expect(summary).toContain('Created');
// Actions
expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true);
@@ -159,42 +164,6 @@ describe('RunnerList', () => {
});
});
- describe('Table data formatting', () => {
- let mockRunnersCopy;
-
- beforeEach(() => {
- mockRunnersCopy = [
- {
- ...mockRunners[0],
- },
- ];
- });
-
- it('Formats job counts', () => {
- mockRunnersCopy[0].jobCount = 1;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mountExtended);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1');
- });
-
- it('Formats large job counts', () => {
- mockRunnersCopy[0].jobCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mountExtended);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
- });
-
- it('Formats large job counts with a plus symbol', () => {
- mockRunnersCopy[0].jobCount = 1001;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mountExtended);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
- });
- });
-
it('Shows runner identifier', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
diff --git a/spec/frontend/runner/components/runner_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js
index 18cfcfae864..c1c7351aab2 100644
--- a/spec/frontend/runner/components/runner_paused_badge_spec.js
+++ b/spec/frontend/runner/components/runner_paused_badge_spec.js
@@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { I18N_PAUSED } from '~/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
@@ -29,8 +30,8 @@ describe('RunnerTypeBadge', () => {
});
it('renders paused state', () => {
- expect(wrapper.text()).toBe('paused');
- expect(findBadge().props('variant')).toBe('danger');
+ expect(wrapper.text()).toBe(I18N_PAUSED);
+ expect(findBadge().props('variant')).toBe('warning');
});
it('renders tooltip', () => {
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
index c988fb8477d..eca042cae86 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,7 +8,9 @@ import { createAlert } from '~/flash';
import { sprintf } from '~/locale';
import {
I18N_ASSIGNED_PROJECTS,
- I18N_NONE,
+ I18N_CLEAR_FILTER_PROJECTS,
+ I18N_FILTER_PROJECTS,
+ I18N_NO_PROJECTS_FOUND,
RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
} from '~/runner/constants';
import RunnerProjects from '~/runner/components/runner_projects.vue';
@@ -35,6 +37,7 @@ describe('RunnerProjects', () => {
const findHeading = () => wrapper.find('h3');
const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader);
+ const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem);
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
@@ -64,10 +67,21 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1);
expect(mockRunnerProjectsQuery).toHaveBeenCalledWith({
id: mockRunner.id,
+ search: '',
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
});
});
+ it('Shows a filter box', () => {
+ createComponent();
+
+ expect(findGlSearchBoxByType().attributes()).toMatchObject({
+ clearbuttontitle: I18N_CLEAR_FILTER_PROJECTS,
+ debounce: '500',
+ placeholder: I18N_FILTER_PROJECTS,
+ });
+ });
+
describe('When there are projects assigned', () => {
beforeEach(async () => {
mockRunnerProjectsQuery.mockResolvedValueOnce(runnerProjectsData);
@@ -110,6 +124,7 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2);
expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
id: mockRunner.id,
+ search: '',
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
after: 'AFTER_CURSOR',
});
@@ -123,10 +138,51 @@ describe('RunnerProjects', () => {
expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3);
expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
id: mockRunner.id,
+ search: '',
last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
before: 'BEFORE_CURSOR',
});
});
+
+ it('When user filters after paginating, the first page is requested', async () => {
+ findGlSearchBoxByType().vm.$emit('input', 'my search');
+ await waitForPromises();
+
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3);
+ expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
+ id: mockRunner.id,
+ search: 'my search',
+ first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
+ });
+ });
+ });
+
+ describe('When user filters', () => {
+ it('Filtered results are requested', async () => {
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1);
+
+ findGlSearchBoxByType().vm.$emit('input', 'my search');
+ await waitForPromises();
+
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({
+ id: mockRunner.id,
+ search: 'my search',
+ first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
+ });
+ });
+
+ it('Filtered results are not requested for short searches', async () => {
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1);
+
+ findGlSearchBoxByType().vm.$emit('input', 'm');
+ await waitForPromises();
+
+ findGlSearchBoxByType().vm.$emit('input', 'my');
+ await waitForPromises();
+
+ expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1);
+ });
});
});
@@ -136,10 +192,11 @@ describe('RunnerProjects', () => {
expect(findGlSkeletonLoading().exists()).toBe(true);
- expect(wrapper.findByText(I18N_NONE).exists()).toBe(false);
+ expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(false);
expect(findRunnerAssignedItems().length).toBe(0);
expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findGlSearchBoxByType().props('isLoading')).toBe(true);
});
});
@@ -168,7 +225,7 @@ describe('RunnerProjects', () => {
});
it('Shows a "None" label', () => {
- expect(wrapper.findByText(I18N_NONE).exists()).toBe(true);
+ expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(true);
});
});
diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
new file mode 100644
index 00000000000..1a8aced9292
--- /dev/null
+++ b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
@@ -0,0 +1,39 @@
+import { nextTick } from 'vue';
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+
+describe('RunnerStackedLayoutBanner', () => {
+ let wrapper;
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+
+ const createComponent = ({ ...options } = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(RunnerStackedLayoutBanner, {
+ ...options,
+ });
+ };
+
+ it('Displays a banner', () => {
+ createComponent();
+
+ expect(findBanner().props()).toMatchObject({
+ svgPath: expect.stringContaining('data:image/svg+xml;utf8,'),
+ title: expect.any(String),
+ buttonText: expect.any(String),
+ buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'),
+ });
+ expect(findLocalStorageSync().exists()).toBe(true);
+ });
+
+ it('Does not display a banner when dismissed', async () => {
+ findLocalStorageSync().vm.$emit('input', true);
+
+ await nextTick();
+
+ expect(findBanner().exists()).toBe(false);
+ expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal
+ });
+});
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js
index bb833bd7d5a..9ab6378304f 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/runner/components/runner_status_badge_spec.js
@@ -3,12 +3,16 @@ import { shallowMount } from '@vue/test-utils';
import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
+ I18N_STATUS_ONLINE,
+ I18N_STATUS_NEVER_CONTACTED,
+ I18N_STATUS_OFFLINE,
+ I18N_STATUS_STALE,
+ I18N_NEVER_CONTACTED_TOOLTIP,
+ I18N_STALE_NEVER_CONTACTED_TOOLTIP,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NEVER_CONTACTED,
- I18N_NEVER_CONTACTED_TOOLTIP,
- I18N_STALE_NEVER_CONTACTED_TOOLTIP,
} from '~/runner/constants';
describe('RunnerTypeBadge', () => {
@@ -46,7 +50,7 @@ describe('RunnerTypeBadge', () => {
it('renders online state', () => {
createComponent();
- expect(wrapper.text()).toBe('online');
+ expect(wrapper.text()).toBe(I18N_STATUS_ONLINE);
expect(findBadge().props('variant')).toBe('success');
expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
});
@@ -59,7 +63,7 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('never contacted');
+ expect(wrapper.text()).toBe(I18N_STATUS_NEVER_CONTACTED);
expect(findBadge().props('variant')).toBe('muted');
expect(getTooltip().value).toBe(I18N_NEVER_CONTACTED_TOOLTIP);
});
@@ -72,7 +76,7 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('offline');
+ expect(wrapper.text()).toBe(I18N_STATUS_OFFLINE);
expect(findBadge().props('variant')).toBe('muted');
expect(getTooltip().value).toBe('Runner is offline; last contact was 1 day ago');
});
@@ -85,7 +89,7 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('stale');
+ expect(wrapper.text()).toBe(I18N_STATUS_STALE);
expect(findBadge().props('variant')).toBe('warning');
expect(getTooltip().value).toBe('Runner is stale; last contact was 1 year ago');
});
@@ -98,7 +102,7 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('stale');
+ expect(wrapper.text()).toBe(I18N_STATUS_STALE);
expect(findBadge().props('variant')).toBe('warning');
expect(getTooltip().value).toBe(I18N_STALE_NEVER_CONTACTED_TOOLTIP);
});
@@ -112,7 +116,7 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('online');
+ expect(wrapper.text()).toBe(I18N_STATUS_ONLINE);
expect(getTooltip().value).toBe('Runner is online; last contact was never');
});
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js
index bd05d4b2cfe..391c17f81cb 100644
--- a/spec/frontend/runner/components/runner_tag_spec.js
+++ b/spec/frontend/runner/components/runner_tag_spec.js
@@ -1,6 +1,8 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+
+import { RUNNER_TAG_BADGE_VARIANT } from '~/runner/constants';
import RunnerTag from '~/runner/components/runner_tag.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -48,7 +50,7 @@ describe('RunnerTag', () => {
it('Displays tags with correct style', () => {
expect(findBadge().props()).toMatchObject({
size: 'sm',
- variant: 'neutral',
+ variant: RUNNER_TAG_BADGE_VARIANT,
});
});
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js
index da89a659432..c6bfabdb18a 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/runner/components/runner_tags_spec.js
@@ -34,7 +34,6 @@ describe('RunnerTags', () => {
it('Displays tags with correct style', () => {
expect(findBadge().props('size')).toBe('sm');
- expect(findBadge().props('variant')).toBe('neutral');
});
it('Displays tags with md size', () => {
@@ -50,7 +49,6 @@ describe('RunnerTags', () => {
props: { tagList: null },
});
- expect(wrapper.text()).toBe('');
- expect(findBadge().exists()).toBe(false);
+ expect(wrapper.html()).toEqual('');
});
});
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js
index 7bb0a2e6e2f..fe922fb9d18 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/runner/components/runner_type_badge_spec.js
@@ -2,7 +2,14 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_INSTANCE_TYPE,
+ I18N_GROUP_TYPE,
+ I18N_PROJECT_TYPE,
+} from '~/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
@@ -27,9 +34,9 @@ describe('RunnerTypeBadge', () => {
describe.each`
type | text
- ${INSTANCE_TYPE} | ${'shared'}
- ${GROUP_TYPE} | ${'group'}
- ${PROJECT_TYPE} | ${'specific'}
+ ${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_PROJECT_TYPE}
`('displays $type runner', ({ type, text }) => {
beforeEach(() => {
createComponent({ props: { type } });
@@ -37,7 +44,7 @@ describe('RunnerTypeBadge', () => {
it(`as "${text}" with an "info" variant`, () => {
expect(findBadge().text()).toBe(text);
- expect(findBadge().props('variant')).toBe('info');
+ expect(findBadge().props('variant')).toBe('muted');
});
it('with a tooltip', () => {
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
index 22d2a9e60f7..45ab8684332 100644
--- a/spec/frontend/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -28,7 +28,7 @@ const mockCount = (type, multiplier = 1) => {
describe('RunnerTypeTabs', () => {
let wrapper;
- const findTabs = () => wrapper.findAll(GlTab);
+ const findTabs = () => wrapper.findAllComponents(GlTab);
const findActiveTab = () =>
findTabs()
.filter((tab) => tab.attributes('active') === 'true')
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 3037364d941..7b67a89f989 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -1,6 +1,7 @@
import Vue, { nextTick } from 'vue';
import { GlForm, GlSkeletonLoader } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
+import { __ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -47,6 +48,7 @@ describe('RunnerUpdateForm', () => {
const findSubmit = () => wrapper.find('[type="submit"]');
const findSubmitDisabledAttr = () => findSubmit().attributes('disabled');
+ const findCancelBtn = () => wrapper.findByRole('link', { name: __('Cancel') });
const submitForm = () => findForm().trigger('submit');
const submitFormAndWait = () => submitForm().then(waitForPromises);
@@ -117,6 +119,11 @@ describe('RunnerUpdateForm', () => {
expect(mockRunner).toMatchObject(getFieldsModel());
});
+ it('Form shows a cancel button', () => {
+ expect(runnerUpdateHandler).not.toHaveBeenCalled();
+ expect(findCancelBtn().attributes('href')).toBe(mockRunnerPath);
+ });
+
it('Form prevent multiple submissions', async () => {
await submitForm();
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js
index 7f1f22be94f..4afbe453903 100644
--- a/spec/frontend/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/runner/components/stat/runner_stats_spec.js
@@ -1,13 +1,20 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { s__ } from '~/locale';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
-import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+import {
+ I18N_STATUS_ONLINE,
+ I18N_STATUS_OFFLINE,
+ I18N_STATUS_STALE,
+ INSTANCE_TYPE,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
+} from '~/runner/constants';
describe('RunnerStats', () => {
let wrapper;
- const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat).wrappers;
+ const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat);
const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerStats, {
@@ -46,16 +53,28 @@ describe('RunnerStats', () => {
});
const text = wrapper.text();
- expect(text).toMatch(`${s__('Runners|Online runners')} 3`);
- expect(text).toMatch(`${s__('Runners|Offline runners')} 2`);
- expect(text).toMatch(`${s__('Runners|Stale runners')} 1`);
+ expect(text).toContain(`${I18N_STATUS_ONLINE} 3`);
+ expect(text).toContain(`${I18N_STATUS_OFFLINE} 2`);
+ expect(text).toContain(`${I18N_STATUS_STALE} 1`);
+ });
+
+ it('Skips query for other stats', () => {
+ createComponent({
+ props: {
+ variables: { status: STATUS_ONLINE },
+ },
+ });
+
+ expect(findSingleStats().at(0).props('skip')).toBe(false);
+ expect(findSingleStats().at(1).props('skip')).toBe(true);
+ expect(findSingleStats().at(2).props('skip')).toBe(true);
});
it('Displays all counts for filtered searches', () => {
const mockVariables = { paused: true };
createComponent({ props: { variables: mockVariables } });
- findSingleStats().forEach((stat) => {
+ findSingleStats().wrappers.forEach((stat) => {
expect(stat.props('variables')).toMatchObject(mockVariables);
});
});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 57d64202219..a17502c7eec 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -15,6 +15,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
+import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
@@ -28,6 +29,9 @@ import {
CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT,
+ I18N_STATUS_ONLINE,
+ I18N_STATUS_OFFLINE,
+ I18N_STATUS_STALE,
INSTANCE_TYPE,
GROUP_TYPE,
PARAM_KEY_PAUSED,
@@ -74,6 +78,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
+ const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
@@ -122,6 +127,11 @@ describe('GroupRunnersApp', () => {
wrapper.destroy();
});
+ it('shows the feedback banner', () => {
+ createComponent();
+ expect(findRunnerStackedLayoutBanner().exists()).toBe(true);
+ });
+
it('shows the runner tabs with a runner count for each type', async () => {
await createComponent({ mountFn: mountExtended });
@@ -153,15 +163,10 @@ describe('GroupRunnersApp', () => {
groupFullPath: mockGroupFullPath,
});
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Online runners')} ${mockGroupRunnersCount}`,
- );
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Offline runners')} ${mockGroupRunnersCount}`,
- );
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Stale runners')} ${mockGroupRunnersCount}`,
- );
+ const text = findRunnerStats().text();
+ expect(text).toContain(`${I18N_STATUS_ONLINE} ${mockGroupRunnersCount}`);
+ expect(text).toContain(`${I18N_STATUS_OFFLINE} ${mockGroupRunnersCount}`);
+ expect(text).toContain(`${I18N_STATUS_STALE} ${mockGroupRunnersCount}`);
});
it('shows the runners list', async () => {
@@ -396,4 +401,36 @@ describe('GroupRunnersApp', () => {
});
});
});
+
+ describe('when user has permission to register group runner', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ groupFullPath: mockGroupFullPath,
+ groupRunnersLimitedCount: mockGroupRunnersCount,
+ },
+ });
+ });
+
+ it('shows the register group runner button', () => {
+ expect(findRegistrationDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when user has no permission to register group runner', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ registrationToken: null,
+ groupFullPath: mockGroupFullPath,
+ groupRunnersLimitedCount: mockGroupRunnersCount,
+ },
+ });
+ });
+
+ it('does not show the register group runner button', () => {
+ expect(findRegistrationDropdown().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/runner_edit/runner_edit_app_spec.js
index ffe3599ac64..fb118817d51 100644
--- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
+++ b/spec/frontend/runner/runner_edit/runner_edit_app_spec.js
@@ -9,8 +9,9 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql';
-import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue';
+import RunnerEditApp from '~//runner/runner_edit/runner_edit_app.vue';
import { captureException } from '~/runner/sentry_utils';
+import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/runner/constants';
import { runnerFormData } from '../mock_data';
@@ -24,7 +25,7 @@ const mockRunnerPath = `/admin/runners/${mockRunnerId}`;
Vue.use(VueApollo);
-describe('AdminRunnerEditApp', () => {
+describe('RunnerEditApp', () => {
let wrapper;
let mockRunnerQuery;
@@ -32,7 +33,7 @@ describe('AdminRunnerEditApp', () => {
const findRunnerUpdateForm = () => wrapper.findComponent(RunnerUpdateForm);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
- wrapper = mountFn(AdminRunnerEditApp, {
+ wrapper = mountFn(RunnerEditApp, {
apolloProvider: createMockApollo([[runnerFormQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
@@ -69,8 +70,8 @@ describe('AdminRunnerEditApp', () => {
it('displays the runner type and status', async () => {
await createComponentWithApollo({ mountFn: mount });
- expect(findRunnerHeader().text()).toContain(`never contacted`);
- expect(findRunnerHeader().text()).toContain(`shared`);
+ expect(findRunnerHeader().text()).toContain(I18N_STATUS_NEVER_CONTACTED);
+ expect(findRunnerHeader().text()).toContain(I18N_INSTANCE_TYPE);
});
it('displays a loading runner form', () => {
@@ -102,7 +103,7 @@ describe('AdminRunnerEditApp', () => {
it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
- component: 'AdminRunnerEditApp',
+ component: 'RunnerEditApp',
});
});
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/runner/utils_spec.js
index 1db9815dfd8..33de1345f85 100644
--- a/spec/frontend/runner/utils_spec.js
+++ b/spec/frontend/runner/utils_spec.js
@@ -1,4 +1,4 @@
-import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils';
+import { formatJobCount, tableField, getPaginationVariables, parseInterval } from '~/runner/utils';
describe('~/runner/utils', () => {
describe('formatJobCount', () => {
@@ -66,4 +66,15 @@ describe('~/runner/utils', () => {
expect(getPaginationVariables(pagination, pageSize)).toEqual(variables);
});
});
+
+ describe('parseInterval', () => {
+ it.each`
+ case | argument | returnValue
+ ${'parses integer'} | ${'86400'} | ${86400}
+ ${'returns null for undefined'} | ${undefined} | ${null}
+ ${'returns null for null'} | ${null} | ${null}
+ `('$case', ({ argument, returnValue }) => {
+ expect(parseInterval(argument)).toStrictEqual(returnValue);
+ });
+ });
});
diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js
index 39d5ee581ec..c0a8259b4fe 100644
--- a/spec/frontend/search/sidebar/components/radio_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js
@@ -44,7 +44,7 @@ describe('RadioFilter', () => {
});
const findGlRadioButtonGroup = () => wrapper.find(GlFormRadioGroup);
- const findGlRadioButtons = () => findGlRadioButtonGroup().findAll(GlFormRadio);
+ const findGlRadioButtons = () => findGlRadioButtonGroup().findAllComponents(GlFormRadio);
const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map((w) => w.text());
describe('template', () => {
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
index 04520a3e704..0e8eebba3cb 100644
--- a/spec/frontend/search/sort/components/app_spec.js
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -46,7 +46,7 @@ describe('GlobalSearchSort', () => {
const findSortButtonGroup = () => wrapper.find(GlButtonGroup);
const findSortDropdown = () => wrapper.find(GlDropdown);
const findSortDirectionButton = () => wrapper.find(GlButton);
- const findDropdownItems = () => findSortDropdown().findAll(GlDropdownItem);
+ const findDropdownItems = () => findSortDropdown().findAllComponents(GlDropdownItem);
const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text());
describe('template', () => {
diff --git a/spec/frontend/set_status_modal/set_status_form_spec.js b/spec/frontend/set_status_modal/set_status_form_spec.js
new file mode 100644
index 00000000000..8e1623eedf5
--- /dev/null
+++ b/spec/frontend/set_status_modal/set_status_form_spec.js
@@ -0,0 +1,167 @@
+import $ from 'jquery';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import SetStatusForm from '~/set_status_modal/set_status_form.vue';
+import EmojiPicker from '~/emoji/components/picker.vue';
+import { timeRanges } from '~/vue_shared/constants';
+import { sprintf } from '~/locale';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+
+describe('SetStatusForm', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ defaultEmoji: 'speech_balloon',
+ emoji: 'thumbsup',
+ message: 'Foo bar',
+ availability: false,
+ };
+
+ const createComponent = async ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(SetStatusForm, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findMessageInput = () =>
+ wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder);
+ const findSelectedEmoji = (emoji) =>
+ wrapper.findByTestId('selected-emoji').find(`gl-emoji[data-name="${emoji}"]`);
+
+ it('sets up emoji autocomplete for the message input', async () => {
+ const gfmAutoCompleteSetupSpy = jest.spyOn(GfmAutoComplete.prototype, 'setup');
+
+ await createComponent();
+
+ expect(gfmAutoCompleteSetupSpy).toHaveBeenCalledWith($(findMessageInput().element), {
+ emojis: true,
+ });
+ });
+
+ describe('when emoji is set', () => {
+ it('displays emoji', async () => {
+ await createComponent();
+
+ expect(findSelectedEmoji(defaultPropsData.emoji).exists()).toBe(true);
+ });
+ });
+
+ describe('when emoji is not set and message is changed', () => {
+ it('displays default emoji', async () => {
+ await createComponent({
+ propsData: {
+ emoji: '',
+ },
+ });
+
+ await findMessageInput().trigger('keyup');
+
+ expect(findSelectedEmoji(defaultPropsData.defaultEmoji).exists()).toBe(true);
+ });
+ });
+
+ describe('when message is set', () => {
+ it('displays filled in message input', async () => {
+ await createComponent();
+
+ expect(findMessageInput().element.value).toBe(defaultPropsData.message);
+ });
+ });
+
+ describe('when clear status after is set', () => {
+ it('displays value in dropdown toggle button', async () => {
+ const clearStatusAfter = timeRanges[0];
+
+ await createComponent({
+ propsData: {
+ clearStatusAfter,
+ },
+ });
+
+ expect(wrapper.findByRole('button', { name: clearStatusAfter.label }).exists()).toBe(true);
+ });
+ });
+
+ describe('when emoji is changed', () => {
+ beforeEach(async () => {
+ await createComponent();
+
+ wrapper.findComponent(EmojiPicker).vm.$emit('click', defaultPropsData.emoji);
+ });
+
+ it('emits `emoji-click` event', () => {
+ expect(wrapper.emitted('emoji-click')).toEqual([[defaultPropsData.emoji]]);
+ });
+ });
+
+ describe('when message is changed', () => {
+ it('emits `message-input` event', async () => {
+ await createComponent();
+
+ const newMessage = 'Foo bar baz';
+
+ await findMessageInput().setValue(newMessage);
+
+ expect(wrapper.emitted('message-input')).toEqual([[newMessage]]);
+ });
+ });
+
+ describe('when availability checkbox is changed', () => {
+ it('emits `availability-input` event', async () => {
+ await createComponent();
+
+ await wrapper
+ .findByLabelText(
+ `${SetStatusForm.i18n.availabilityCheckboxLabel} ${SetStatusForm.i18n.availabilityCheckboxHelpText}`,
+ )
+ .setChecked();
+
+ expect(wrapper.emitted('availability-input')).toEqual([[true]]);
+ });
+ });
+
+ describe('when `Clear status after` dropdown is changed', () => {
+ it('emits `clear-status-after-click`', async () => {
+ await wrapper.findByTestId('thirtyMinutes').trigger('click');
+
+ expect(wrapper.emitted('clear-status-after-click')).toEqual([[timeRanges[0]]]);
+ });
+ });
+
+ describe('when clear status button is clicked', () => {
+ beforeEach(async () => {
+ await createComponent();
+
+ await wrapper
+ .findByRole('button', { name: SetStatusForm.i18n.clearStatusButtonLabel })
+ .trigger('click');
+ });
+
+ it('clears emoji and message', () => {
+ expect(wrapper.emitted('emoji-click')).toEqual([['']]);
+ expect(wrapper.emitted('message-input')).toEqual([['']]);
+ expect(wrapper.findByTestId('no-emoji-placeholder').exists()).toBe(true);
+ });
+ });
+
+ describe('when `currentClearStatusAfter` prop is set', () => {
+ it('displays clear status message', async () => {
+ const date = '2022-08-25 21:14:48 UTC';
+
+ await createComponent({
+ propsData: {
+ currentClearStatusAfter: date,
+ },
+ });
+
+ expect(
+ wrapper.findByText(sprintf(SetStatusForm.i18n.clearStatusAfterMessage, { date })).exists(),
+ ).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index e3b5478290a..c5fb590646d 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -1,14 +1,14 @@
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
import createFlash from '~/flash';
import stubChildren from 'helpers/stub_children';
-import SetStatusModalWrapper, {
- AVAILABILITY_STATUS,
-} from '~/set_status_modal/set_status_modal_wrapper.vue';
+import SetStatusModalWrapper from '~/set_status_modal/set_status_modal_wrapper.vue';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
+import SetStatusForm from '~/set_status_modal/set_status_form.vue';
jest.mock('~/flash');
@@ -33,7 +33,7 @@ describe('SetStatusModalWrapper', () => {
};
const createComponent = (props = {}) => {
- return mount(SetStatusModalWrapper, {
+ return mountExtended(SetStatusModalWrapper, {
propsData: {
...defaultProps,
...props,
@@ -42,6 +42,7 @@ describe('SetStatusModalWrapper', () => {
...stubChildren(SetStatusModalWrapper),
GlFormInput: false,
GlFormInputGroup: false,
+ SetStatusForm: false,
EmojiPicker: EmojiPickerStub,
},
mocks: {
@@ -51,7 +52,8 @@ describe('SetStatusModalWrapper', () => {
};
const findModal = () => wrapper.find(GlModal);
- const findFormField = (field) => wrapper.find(`[name="user[status][${field}]"]`);
+ const findMessageField = () =>
+ wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
@@ -81,14 +83,8 @@ describe('SetStatusModalWrapper', () => {
return initModal();
});
- it('sets the hidden status emoji field', () => {
- const field = findFormField('emoji');
- expect(field.exists()).toBe(true);
- expect(field.element.value).toBe(defaultEmoji);
- });
-
it('sets the message field', () => {
- const field = findFormField('message');
+ const field = findMessageField();
expect(field.exists()).toBe(true);
expect(field.element.value).toBe(defaultMessage);
});
@@ -118,10 +114,10 @@ describe('SetStatusModalWrapper', () => {
});
});
- it('sets emojiTag when clicking in emoji picker', async () => {
+ it('passes emoji to `SetStatusForm`', async () => {
await getEmojiPicker().vm.$emit('click', 'thumbsup');
- expect(wrapper.vm.emojiTag).toContain('data-name="thumbsup"');
+ expect(wrapper.findComponent(SetStatusForm).props('emoji')).toBe('thumbsup');
});
});
@@ -133,7 +129,7 @@ describe('SetStatusModalWrapper', () => {
});
it('does not set the message field', () => {
- expect(findFormField('message').element.value).toBe('');
+ expect(findMessageField().element.value).toBe('');
});
it('hides the clear status button', () => {
@@ -141,18 +137,6 @@ describe('SetStatusModalWrapper', () => {
});
});
- describe('with no currentEmoji set', () => {
- beforeEach(async () => {
- await initEmojiMock();
- wrapper = createComponent({ currentEmoji: '' });
- return initModal();
- });
-
- it('does not set the hidden status emoji field', () => {
- expect(findFormField('emoji').element.value).toBe('');
- });
- });
-
describe('with currentClearStatusAfter set', () => {
beforeEach(async () => {
await initEmojiMock();
@@ -182,8 +166,7 @@ describe('SetStatusModalWrapper', () => {
findModal().vm.$emit('secondary');
await nextTick();
- expect(findFormField('message').element.value).toBe('');
- expect(findFormField('emoji').element.value).toBe('');
+ expect(findMessageField().element.value).toBe('');
});
it('clicking "setStatus" submits the user status', async () => {
@@ -194,7 +177,7 @@ describe('SetStatusModalWrapper', () => {
findAvailabilityCheckbox().vm.$emit('input', true);
// set the currentClearStatusAfter to 30 minutes
- wrapper.find('[data-testid="thirtyMinutes"]').vm.$emit('click');
+ wrapper.find('[data-testid="thirtyMinutes"]').trigger('click');
findModal().vm.$emit('primary');
await nextTick();
diff --git a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
new file mode 100644
index 00000000000..eaee0e77311
--- /dev/null
+++ b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
@@ -0,0 +1,156 @@
+import { nextTick } from 'vue';
+import { cloneDeep } from 'lodash';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { resetHTMLFixture } from 'helpers/fixtures';
+import { useFakeDate } from 'helpers/fake_date';
+import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
+import SetStatusForm from '~/set_status_modal/set_status_form.vue';
+import { TIME_RANGES_WITH_NEVER, NEVER_TIME_RANGE } from '~/set_status_modal/constants';
+
+describe('UserProfileSetStatusWrapper', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ fields: {
+ emoji: { name: 'user[status][emoji]', id: 'user_status_emoji', value: '8ball' },
+ message: { name: 'user[status][message]', id: 'user_status_message', value: 'foo bar' },
+ availability: {
+ name: 'user[status][availability]',
+ id: 'user_status_availability',
+ value: 'busy',
+ },
+ clearStatusAfter: {
+ name: 'user[status][clear_status_after]',
+ id: 'user_status_clear_status_after',
+ value: '2022-09-03 03:06:26 UTC',
+ },
+ },
+ };
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = mountExtended(UserProfileSetStatusWrapper, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ });
+ };
+
+ const findInput = (name) => wrapper.find(`[name="${name}"]`);
+ const findSetStatusForm = () => wrapper.findComponent(SetStatusForm);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders `SetStatusForm` component and passes expected props', () => {
+ createComponent();
+
+ expect(cloneDeep(findSetStatusForm().props())).toMatchObject({
+ defaultEmoji: 'speech_balloon',
+ emoji: defaultProvide.fields.emoji.value,
+ message: defaultProvide.fields.message.value,
+ availability: true,
+ clearStatusAfter: NEVER_TIME_RANGE,
+ currentClearStatusAfter: defaultProvide.fields.clearStatusAfter.value,
+ });
+ });
+
+ it.each`
+ input
+ ${'emoji'}
+ ${'message'}
+ ${'availability'}
+ `('renders hidden $input input with value set', ({ input }) => {
+ createComponent();
+
+ expect(findInput(defaultProvide.fields[input].name).attributes('value')).toBe(
+ defaultProvide.fields[input].value,
+ );
+ });
+
+ describe('when clear status after dropdown is set to `Never`', () => {
+ it('renders hidden clear status after input with value unset', () => {
+ createComponent();
+
+ expect(
+ findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value'),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('when clear status after dropdown has a value selected', () => {
+ it('renders hidden clear status after input with value set', async () => {
+ createComponent();
+
+ findSetStatusForm().vm.$emit('clear-status-after-click', TIME_RANGES_WITH_NEVER[1]);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value')).toBe(
+ TIME_RANGES_WITH_NEVER[1].shortcut,
+ );
+ });
+ });
+
+ describe('when emoji is changed', () => {
+ it('updates hidden emoji input value', async () => {
+ createComponent();
+
+ const newEmoji = 'basketball';
+
+ findSetStatusForm().vm.$emit('emoji-click', newEmoji);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.emoji.name).attributes('value')).toBe(newEmoji);
+ });
+ });
+
+ describe('when message is changed', () => {
+ it('updates hidden message input value', async () => {
+ createComponent();
+
+ const newMessage = 'foo bar baz';
+
+ findSetStatusForm().vm.$emit('message-input', newMessage);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.message.name).attributes('value')).toBe(newMessage);
+ });
+ });
+
+ describe('when form is successfully submitted', () => {
+ // 2022-09-02 00:00:00 UTC
+ useFakeDate(2022, 8, 2);
+
+ const form = document.createElement('form');
+ form.classList.add('js-edit-user');
+
+ beforeEach(async () => {
+ document.body.appendChild(form);
+ createComponent();
+
+ const oneDay = TIME_RANGES_WITH_NEVER[4];
+
+ findSetStatusForm().vm.$emit('clear-status-after-click', oneDay);
+
+ await nextTick();
+
+ form.dispatchEvent(new Event('ajax:success'));
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('updates clear status after dropdown to `Never`', () => {
+ expect(findSetStatusForm().props('clearStatusAfter')).toBe(NEVER_TIME_RANGE);
+ });
+
+ it('updates `currentClearStatusAfter` prop', () => {
+ expect(findSetStatusForm().props('currentClearStatusAfter')).toBe('2022-09-03 00:00:00 UTC');
+ });
+ });
+});
diff --git a/spec/frontend/set_status_modal/utils_spec.js b/spec/frontend/set_status_modal/utils_spec.js
index 273f30f8311..1e918b75a98 100644
--- a/spec/frontend/set_status_modal/utils_spec.js
+++ b/spec/frontend/set_status_modal/utils_spec.js
@@ -1,4 +1,5 @@
-import { AVAILABILITY_STATUS, isUserBusy } from '~/set_status_modal/utils';
+import { isUserBusy } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
describe('Set status modal utils', () => {
describe('isUserBusy', () => {
diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/assignee_title_spec.js
index 3079cb28406..e29e3d489a5 100644
--- a/spec/frontend/sidebar/assignee_title_spec.js
+++ b/spec/frontend/sidebar/assignee_title_spec.js
@@ -85,7 +85,7 @@ describe('AssigneeTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('renders spinner when loading', () => {
@@ -95,7 +95,7 @@ describe('AssigneeTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('does not render edit link when not editable', () => {
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 517b4f12559..8cde70ff8da 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -143,7 +143,7 @@ describe('AssigneeAvatarLink component', () => {
issuableType | userId
${'merge_request'} | ${undefined}
${'issue'} | ${'1'}
- `('it sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => {
+ `('sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => {
createComponent({
issuableType,
});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index 5aa8264b98c..81ff51133bf 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -23,7 +23,7 @@ describe('CollapsedAssigneeList component', () => {
const findNoUsersIcon = () => wrapper.find(GlIcon);
const findAvatarCounter = () => wrapper.find('.avatar-counter');
- const findAssignees = () => wrapper.findAll(CollapsedAssignee);
+ const findAssignees = () => wrapper.findAllComponents(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('title');
afterEach(() => {
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 88015ed42a3..3644a51c7fd 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -65,6 +65,7 @@ describe('Sidebar assignees widget', () => {
issuableId: 0,
fullPath: '/mygroup/myProject',
allowMultipleAssignees: true,
+ editable: true,
...props,
},
provide: {
@@ -350,6 +351,17 @@ describe('Sidebar assignees widget', () => {
});
});
+ describe('when issuable is not editable by the user', () => {
+ beforeEach(async () => {
+ createComponent({ props: { editable: false } });
+ await waitForPromises();
+ });
+
+ it('passes editable prop as false to IssuableAssignees', () => {
+ expect(findAssignees().props('editable')).toBe(false);
+ });
+ });
+
it('includes the real-time assignees component', async () => {
createComponent();
await waitForPromises();
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index f7437386814..b902d7313fd 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -42,7 +42,7 @@ describe('UncollapsedAssigneeList component', () => {
});
it('only has one user', () => {
- expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1);
+ expect(wrapper.findAllComponents(AssigneeAvatarLink).length).toBe(1);
});
it('calls the AssigneeAvatarLink with the proper props', () => {
@@ -79,7 +79,7 @@ describe('UncollapsedAssigneeList component', () => {
});
it('shows truncated users', () => {
- expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT);
+ expect(wrapper.findAllComponents(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT);
});
describe('when more button is clicked', () => {
@@ -94,7 +94,9 @@ describe('UncollapsedAssigneeList component', () => {
});
it('shows all users', () => {
- expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1);
+ expect(wrapper.findAllComponents(AssigneeAvatarLink).length).toBe(
+ DEFAULT_RENDER_COUNT + 1,
+ );
});
});
});
diff --git a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
index 4dbf3d426bb..37c16bc9235 100644
--- a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
+++ b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
const name = 'Administrator';
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
index 7775ed6aa37..1ea035c7184 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -71,12 +71,7 @@ describe('Sidebar Confidentiality Form', () => {
it('creates a flash if mutation contains errors', async () => {
createComponent({
mutate: jest.fn().mockResolvedValue({
- data: {
- issuableSetConfidential: {
- issuable: { confidential: false },
- errors: ['Houston, we have a problem!'],
- },
- },
+ data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } },
}),
});
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
@@ -87,24 +82,6 @@ describe('Sidebar Confidentiality Form', () => {
});
});
- it('emits `closeForm` event with confidentiality value when mutation is successful', async () => {
- createComponent({
- mutate: jest.fn().mockResolvedValue({
- data: {
- issuableSetConfidential: {
- issuable: { confidential: true },
- errors: [],
- },
- },
- }),
- });
-
- findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
- await waitForPromises();
-
- expect(wrapper.emitted('closeForm')).toEqual([[{ confidential: true }]]);
- });
-
describe('when issue is not confidential', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
index 18ee423d12e..3a3f0b1d9fa 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -132,7 +132,6 @@ describe('Sidebar Confidentiality Widget', () => {
it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
createComponent();
const el = wrapper.vm.$el;
- const closeFormPayload = { confidential: true };
jest.spyOn(el, 'dispatchEvent');
await waitForPromises();
@@ -141,12 +140,12 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().isVisible()).toBe(true);
- findConfidentialityForm().vm.$emit('closeForm', closeFormPayload);
+ findConfidentialityForm().vm.$emit('closeForm');
await nextTick();
expect(findConfidentialityForm().isVisible()).toBe(false);
expect(el.dispatchEvent).toHaveBeenCalled();
- expect(wrapper.emitted('closeForm')).toEqual([[closeFormPayload]]);
+ expect(wrapper.emitted('closeForm')).toEqual([[]]);
});
it('emits `expandSidebar` event when it is emitted from child component', async () => {
diff --git a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
index fda21e06987..a7556b9110c 100644
--- a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
@@ -5,10 +5,10 @@ import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.v
describe('SidebarInheritDate', () => {
let wrapper;
- const findFixedFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(0);
- const findInheritFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(1);
- const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0);
- const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1);
+ const findFixedFormattedDate = () => wrapper.findAllComponents(SidebarFormattedDate).at(0);
+ const findInheritFormattedDate = () => wrapper.findAllComponents(SidebarFormattedDate).at(1);
+ const findFixedRadio = () => wrapper.findAllComponents(GlFormRadio).at(0);
+ const findInheritRadio = () => wrapper.findAllComponents(GlFormRadio).at(1);
const createComponent = ({ dueDateIsFixed = false } = {}) => {
wrapper = shallowMount(SidebarInheritDate, {
@@ -36,8 +36,8 @@ describe('SidebarInheritDate', () => {
});
it('displays formatted fixed and inherited dates with radio buttons', () => {
- expect(wrapper.findAll(SidebarFormattedDate)).toHaveLength(2);
- expect(wrapper.findAll(GlFormRadio)).toHaveLength(2);
+ expect(wrapper.findAllComponents(SidebarFormattedDate)).toHaveLength(2);
+ expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(2);
expect(findFixedFormattedDate().props('formattedDate')).toBe('Apr 15, 2021');
expect(findInheritFormattedDate().props('formattedDate')).toBe('May 15, 2021');
expect(findFixedRadio().text()).toBe('Fixed:');
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 2c24df2436a..d00c8dcb653 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -52,7 +52,7 @@ describe('UncollapsedReviewerList component', () => {
});
it('only has one user', () => {
- expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(1);
+ expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(1);
});
it('shows one user with avatar, and author name', () => {
@@ -96,7 +96,7 @@ describe('UncollapsedReviewerList component', () => {
});
it('has both users', () => {
- expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(2);
+ expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(2);
});
it('shows both users with avatar, and author name', () => {
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 5d80a221d8e..83eb9a18597 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -97,7 +97,7 @@ describe('SidebarSeverity', () => {
});
});
- it('shows error alert when severity update fails ', async () => {
+ it('shows error alert when severity update fails', async () => {
const errorMsg = 'Something went wrong';
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg);
findCriticalSeverityDropdownItem().vm.$emit('click');
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 8ebd2dabfc2..6761731c093 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -238,6 +238,24 @@ describe('SidebarDropdownWidget', () => {
expect(findSelectedAttribute().text()).toBe('None');
});
});
+
+ describe("when user doesn't have permission to view current attribute", () => {
+ it('renders no permission text', () => {
+ createComponent({
+ data: {
+ hasCurrentAttribute: true,
+ currentAttribute: null,
+ },
+ queries: {
+ currentAttribute: { loading: false },
+ },
+ });
+
+ expect(findSelectedAttribute().text()).toBe(
+ `You don't have permission to view this ${wrapper.props('issuableAttribute')}.`,
+ );
+ });
+ });
});
describe('when a user can edit', () => {
diff --git a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
index 9a68940590d..430acf9f9e7 100644
--- a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
@@ -156,7 +156,7 @@ describe('Sidebar Subscriptions Widget', () => {
});
await waitForPromises();
- await wrapper.find('.dropdown-item').trigger('click');
+ await wrapper.find('[data-testid="notifications-toggle"]').vm.$emit('change');
await waitForPromises();
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 5ed8810e95e..4e619a4e609 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -161,7 +161,6 @@ describe('Issuable Time Tracking Report', () => {
id: timelogToRemoveId,
},
},
- update: expect.anything(),
});
});
@@ -179,7 +178,6 @@ describe('Issuable Time Tracking Report', () => {
id: timelogToRemoveId,
},
},
- update: expect.anything(),
});
expect(createFlash).toHaveBeenCalledWith({
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/issuable_assignees_spec.js
index 3563d478f3f..dc59b68bbd4 100644
--- a/spec/frontend/sidebar/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/issuable_assignees_spec.js
@@ -12,6 +12,7 @@ describe('IssuableAssignees', () => {
},
propsData: {
users: [],
+ editable: true,
...props,
},
});
@@ -25,15 +26,19 @@ describe('IssuableAssignees', () => {
});
describe('when no assignees are present', () => {
- it('renders "None - assign yourself" when user is logged in', () => {
- createComponent({ signedIn: true });
- expect(findEmptyAssignee().text()).toBe('None - assign yourself');
- });
-
- it('renders "None" when user is not logged in', () => {
- createComponent();
- expect(findEmptyAssignee().text()).toBe('None');
- });
+ it.each`
+ signedIn | editable | message
+ ${true} | ${true} | ${'None - assign yourself'}
+ ${true} | ${false} | ${'None'}
+ ${false} | ${true} | ${'None'}
+ ${false} | ${false} | ${'None'}
+ `(
+ 'renders "$message" when signedIn is $signedIn and editable is $editable',
+ ({ signedIn, editable, message }) => {
+ createComponent({ signedIn, editable });
+ expect(findEmptyAssignee().text()).toBe(message);
+ },
+ );
});
describe('when assignees are present', () => {
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
index bb757fdf63b..986ccaea4b6 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
@@ -130,7 +130,7 @@ describe('IssuableLockForm', () => {
expect(findEditForm().exists()).toBe(true);
});
- it('tracks the event ', () => {
+ it('tracks the event', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(findEditLink().element);
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 9c6e23e928c..2afe9647cbe 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -497,6 +497,11 @@ export const searchResponse = {
user: mockUser2,
},
],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ startCursor: null,
+ },
},
},
},
@@ -559,6 +564,11 @@ export const projectMembersResponse = {
},
},
],
+ pageInfo: {
+ hasNextPage: false,
+ startCursor: null,
+ endCursor: null,
+ },
},
},
},
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index e32694abcce..355f0c45bbe 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -27,7 +27,7 @@ describe('Sidebar mediator', () => {
mock.restore();
});
- it('assigns yourself ', () => {
+ it('assigns yourself', () => {
mediator.assignYourself();
expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index 7bb7b18adf8..2e6807ed9d8 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -7,6 +7,7 @@ import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
import Mock from './mock_data';
jest.mock('~/flash');
@@ -75,7 +76,9 @@ describe('SidebarMoveIssue', () => {
it('should initialize the deprecatedJQueryDropdown', () => {
test.sidebarMoveIssue.initDropdown();
- expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy();
+ expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeInstanceOf(
+ GitLabDropdown,
+ );
});
it('escapes html from project name', async () => {
@@ -97,7 +100,7 @@ describe('SidebarMoveIssue', () => {
test.sidebarMoveIssue.onConfirmClicked();
expect(test.mediator.moveIssue).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBeTruthy();
+ expect(test.$confirmButton.prop('disabled')).toBe(true);
expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
});
@@ -113,7 +116,7 @@ describe('SidebarMoveIssue', () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBeFalsy();
+ expect(test.$confirmButton.prop('disabled')).toBe(false);
expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
});
@@ -139,7 +142,7 @@ describe('SidebarMoveIssue', () => {
test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
- expect(test.$confirmButton.prop('disabled')).toBeTruthy();
+ expect(test.$confirmButton.prop('disabled')).toBe(true);
});
it('should set moveToProjectId on dropdown item click', async () => {
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index 9316268d2ad..5f696b237e0 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -55,7 +55,7 @@ describe('SidebarTodo', () => {
wrapper.find('button').trigger('click');
await nextTick();
- expect(wrapper.emitted().toggleTodo).toBeTruthy();
+ expect(wrapper.emitted().toggleTodo).toHaveLength(1);
});
it('renders component container element with proper data attributes', () => {
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index f49ceb2fede..cf897414ccb 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -16,10 +16,10 @@ import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_e
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import {
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ VISIBILITY_LEVEL_INTERNAL_STRING,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
import CreateSnippetMutation from '~/snippets/mutations/create_snippet.mutation.graphql';
import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.graphql';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
@@ -41,7 +41,7 @@ const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42';
const createSnippet = () =>
merge(createGQLSnippet(), {
webUrl: TEST_WEB_URL,
- visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+ visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING,
});
const createQueryResponse = (obj = {}) =>
@@ -70,7 +70,7 @@ const getApiData = ({
id,
title = '',
description = '',
- visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
+ visibilityLevel = VISIBILITY_LEVEL_PRIVATE_STRING,
} = {}) => ({
id,
title,
@@ -128,7 +128,10 @@ describe('Snippet Edit app', () => {
const setDescription = (val) =>
wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val);
- const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => {
+ const createComponent = ({
+ props = {},
+ selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING,
+ } = {}) => {
if (wrapper) {
throw new Error('wrapper already created');
}
@@ -260,17 +263,18 @@ describe('Snippet Edit app', () => {
},
);
- it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
- 'marks %s visibility by default',
- async (visibility) => {
- createComponent({
- props: { snippetGid: '' },
- selectedLevel: visibility,
- });
+ it.each([
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ VISIBILITY_LEVEL_INTERNAL_STRING,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+ ])('marks %s visibility by default', async (visibility) => {
+ createComponent({
+ props: { snippetGid: '' },
+ selectedLevel: visibility,
+ });
- expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility);
- },
- );
+ expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility);
+ });
describe('form submission handling', () => {
describe('when creating a new snippet', () => {
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b29ed97099f..032dcf8e5f5 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -7,10 +7,10 @@ import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
import {
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
+ VISIBILITY_LEVEL_INTERNAL_STRING,
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import { stubPerformanceWebAPI } from 'helpers/performance';
@@ -69,7 +69,7 @@ describe('Snippet view app', () => {
createComponent({
data: {
snippet: {
- visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
+ visibilityLevel: VISIBILITY_LEVEL_PUBLIC_STRING,
webUrl: 'http://foo.bar',
},
},
@@ -85,7 +85,7 @@ describe('Snippet view app', () => {
},
},
});
- const blobs = wrapper.findAll(SnippetBlob);
+ const blobs = wrapper.findAllComponents(SnippetBlob);
expect(blobs.length).toBe(2);
expect(blobs.at(0).props('blob')).toEqual(Blob);
expect(blobs.at(1).props('blob')).toEqual(BinaryBlob);
@@ -93,11 +93,11 @@ describe('Snippet view app', () => {
describe('Embed dropdown rendering', () => {
it.each`
- visibilityLevel | condition | isRendered
- ${SNIPPET_VISIBILITY_INTERNAL} | ${'not render'} | ${false}
- ${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false}
- ${'foo'} | ${'not render'} | ${false}
- ${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true}
+ visibilityLevel | condition | isRendered
+ ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${'not render'} | ${false}
+ ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${'not render'} | ${false}
+ ${'foo'} | ${'not render'} | ${false}
+ ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'render'} | ${true}
`('does $condition embed-dropdown by default', ({ visibilityLevel, isRendered }) => {
createComponent({
data: {
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index df98312b498..a650353093d 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -32,7 +32,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
};
const findLabel = () => wrapper.findComponent(GlFormGroup);
- const findBlobEdits = () => wrapper.findAll(SnippetBlobEdit);
+ const findBlobEdits = () => wrapper.findAllComponents(SnippetBlobEdit);
const findBlobsData = () =>
findBlobEdits().wrappers.map((x) => ({
blob: x.props('blob'),
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index c395112e313..aa31377f390 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -15,7 +15,7 @@ import {
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
-import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
describe('Blob Embeddable', () => {
@@ -23,7 +23,7 @@ describe('Blob Embeddable', () => {
const snippet = {
id: 'gid://foo.bar/snippet',
webUrl: 'https://foo.bar',
- visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
+ visibilityLevel: VISIBILITY_LEVEL_PUBLIC_STRING,
};
const dataMock = {
activeViewerType: SimpleViewerMock.type,
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 62d1ac9b476..2d043a5caba 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -2,10 +2,12 @@ import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import {
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ VISIBILITY_LEVEL_INTERNAL_STRING,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
+import {
SNIPPET_VISIBILITY,
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
SNIPPET_LEVELS_RESTRICTED,
SNIPPET_LEVELS_DISABLED,
} from '~/snippets/constants';
@@ -38,7 +40,7 @@ describe('Snippet Visibility Edit component', () => {
}
const findLink = () => wrapper.find('label').find(GlLink);
- const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
+ const findRadios = () => wrapper.find(GlFormRadioGroup).findAllComponents(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map((x) => {
return {
@@ -75,19 +77,19 @@ describe('Snippet Visibility Edit component', () => {
const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]');
const RESULTING_OPTIONS = {
0: {
- value: SNIPPET_VISIBILITY_PRIVATE,
+ value: VISIBILITY_LEVEL_PRIVATE_STRING,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description,
},
10: {
- value: SNIPPET_VISIBILITY_INTERNAL,
+ value: VISIBILITY_LEVEL_INTERNAL_STRING,
icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
20: {
- value: SNIPPET_VISIBILITY_PUBLIC,
+ value: VISIBILITY_LEVEL_PUBLIC_STRING,
icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
@@ -130,7 +132,7 @@ describe('Snippet Visibility Edit component', () => {
createComponent({ propsData: { isProjectSnippet: true }, deep: true });
expect(findRadiosData()[0]).toEqual({
- value: SNIPPET_VISIBILITY_PRIVATE,
+ value: VISIBILITY_LEVEL_PRIVATE_STRING,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description_project,
@@ -141,7 +143,7 @@ describe('Snippet Visibility Edit component', () => {
describe('functionality', () => {
it('pre-selects correct option in the list', () => {
- const value = SNIPPET_VISIBILITY_INTERNAL;
+ const value = VISIBILITY_LEVEL_INTERNAL_STRING;
createComponent({ propsData: { value } });
diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js
index cd549155914..af91d8aeb6b 100644
--- a/spec/frontend/surveys/merge_request_performance/app_spec.js
+++ b/spec/frontend/surveys/merge_request_performance/app_spec.js
@@ -6,6 +6,17 @@ import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisse
import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue';
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
+const createRenderTrackedArguments = () => [
+ undefined,
+ 'survey:mr_experience',
+ {
+ label: 'render',
+ extra: {
+ accountAge: 0,
+ },
+ },
+];
+
describe('MergeRequestExperienceSurveyApp', () => {
let trackingSpy;
let wrapper;
@@ -24,6 +35,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
dismiss,
shouldShowCallout,
});
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, {
propsData: {
accountAge: 0,
@@ -35,10 +47,13 @@ describe('MergeRequestExperienceSurveyApp', () => {
});
};
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
describe('when user callout is visible', () => {
beforeEach(() => {
createWrapper();
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('shows survey', async () => {
@@ -47,14 +62,46 @@ describe('MergeRequestExperienceSurveyApp', () => {
expect(wrapper.emitted().close).toBe(undefined);
});
- it('triggers user callout on close', async () => {
- findCloseButton().vm.$emit('click');
- expect(dismiss).toHaveBeenCalledTimes(1);
+ it('tracks render once', async () => {
+ expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
});
- it('emits close event on close button click', async () => {
- findCloseButton().vm.$emit('click');
- expect(wrapper.emitted()).toMatchObject({ close: [[]] });
+ it("doesn't track subsequent renders", async () => {
+ createWrapper();
+ expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ });
+
+ describe('when close button clicked', () => {
+ beforeEach(() => {
+ findCloseButton().vm.$emit('click');
+ });
+
+ it('triggers user callout on close', async () => {
+ expect(dismiss).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits close event on close button click', async () => {
+ expect(wrapper.emitted()).toMatchObject({ close: [[]] });
+ });
+
+ it('tracks dismissal', async () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
+ label: 'dismiss',
+ extra: {
+ accountAge: 0,
+ },
+ });
+ });
+
+ it('tracks subsequent renders', async () => {
+ createWrapper();
+ expect(trackingSpy.mock.calls).toEqual([
+ createRenderTrackedArguments(),
+ expect.anything(),
+ createRenderTrackedArguments(),
+ ]);
+ });
});
it('applies correct feature name for user callout', () => {
@@ -135,6 +182,10 @@ describe('MergeRequestExperienceSurveyApp', () => {
it('emits close event', async () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
+
+ it("doesn't track anything", async () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(0);
+ });
});
describe('when Escape key is pressed', () => {
@@ -148,5 +199,14 @@ describe('MergeRequestExperienceSurveyApp', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
expect(dismiss).toHaveBeenCalledTimes(1);
});
+
+ it('tracks dismissal', async () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
+ label: 'dismiss',
+ extra: {
+ accountAge: 0,
+ },
+ });
+ });
});
});
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index 16ffd2b7013..12a44452717 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -134,7 +134,7 @@ describe('StatesTable', () => {
await nextTick();
};
- const findActions = () => wrapper.findAll(StateActions);
+ const findActions = () => wrapper.findAllComponents(StateActions);
beforeEach(() => {
return createComponent();
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 0f121fd1beb..6e2908e659f 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -24,19 +24,6 @@ export const disabledJobTokenScope = {
},
};
-export const updateJobTokenScope = {
- data: {
- ciCdSettingsUpdate: {
- ciCdSettings: {
- jobTokenScopeEnabled: true,
- __typename: 'ProjectCiCdSetting',
- },
- errors: [],
- __typename: 'CiCdSettingsUpdatePayload',
- },
- },
-};
-
export const projectsWithScope = {
data: {
project: {
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index 5aaeebd5af4..024e7dfff8c 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -8,13 +8,11 @@ import createFlash from '~/flash';
import TokenAccess from '~/token_access/components/token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
-import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql';
import getProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql';
import {
enabledJobTokenScope,
disabledJobTokenScope,
- updateJobTokenScope,
projectsWithScope,
addProjectSuccess,
removeProjectSuccess,
@@ -32,7 +30,6 @@ describe('TokenAccess component', () => {
const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope);
const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope);
- const updateJobTokenScopeHandler = jest.fn().mockResolvedValue(updateJobTokenScope);
const getProjectsWithScope = jest.fn().mockResolvedValue(projectsWithScope);
const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess);
const addProjectFailureHandler = jest.fn().mockRejectedValue(error);
@@ -95,7 +92,7 @@ describe('TokenAccess component', () => {
expect(findTokenSection().exists()).toBe(true);
});
- it('the toggle should be disabled and the token section should not show', async () => {
+ it('the toggle should be disabled and the token section should show', async () => {
createComponent([
[getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
@@ -104,28 +101,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(findToggle().props('value')).toBe(false);
- expect(findTokenSection().exists()).toBe(false);
- });
-
- it('switching the toggle calls the mutation and fetches the projects again', async () => {
- createComponent([
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [updateCIJobTokenScopeMutation, updateJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
- ]);
-
- await waitForPromises();
-
- expect(getProjectsWithScope).toHaveBeenCalledTimes(1);
-
- findToggle().vm.$emit('change', true);
-
- await waitForPromises();
-
- expect(updateJobTokenScopeHandler).toHaveBeenCalledWith({
- input: { fullPath: projectPath, jobTokenScopeEnabled: true },
- });
- expect(getProjectsWithScope).toHaveBeenCalledTimes(2);
+ expect(findTokenSection().exists()).toBe(true);
});
});
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
index eef352a72ff..998bb2a9ea2 100644
--- a/spec/frontend/tooltips/components/tooltips_spec.js
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -28,7 +28,7 @@ describe('tooltips/components/tooltips.vue', () => {
return target;
};
- const allTooltips = () => wrapper.findAll(GlTooltip);
+ const allTooltips = () => wrapper.findAllComponents(GlTooltip);
afterEach(() => {
wrapper.destroy();
@@ -68,7 +68,7 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.findAll(GlTooltip)).toHaveLength(1);
+ expect(wrapper.findAllComponents(GlTooltip)).toHaveLength(1);
});
it('sets tooltip content from title attribute', async () => {
diff --git a/spec/frontend/user_lists/store/index/actions_spec.js b/spec/frontend/user_lists/store/index/actions_spec.js
index 4a8d0afb963..7b2e29ae412 100644
--- a/spec/frontend/user_lists/store/index/actions_spec.js
+++ b/spec/frontend/user_lists/store/index/actions_spec.js
@@ -41,7 +41,7 @@ describe('~/user_lists/store/index/actions', () => {
});
describe('success', () => {
- it('dispatches requestUserLists and receiveUserListsSuccess ', () => {
+ it('dispatches requestUserLists and receiveUserListsSuccess', () => {
return testAction(
fetchUserLists,
null,
@@ -61,7 +61,7 @@ describe('~/user_lists/store/index/actions', () => {
});
describe('error', () => {
- it('dispatches requestUserLists and receiveUserListsError ', () => {
+ it('dispatches requestUserLists and receiveUserListsError', () => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
return testAction(
diff --git a/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
index cb53dc1fb61..063425454d7 100644
--- a/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
@@ -1,10 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import AddedCommentMessage from '~/vue_merge_request_widget/components/added_commit_message.vue';
let wrapper;
function factory(propsData) {
- wrapper = shallowMount(AddedCommentMessage, {
+ wrapper = mount(AddedCommentMessage, {
propsData: {
isFastForwardEnabled: false,
targetBranch: 'main',
@@ -23,4 +23,13 @@ describe('Widget added commit message', () => {
expect(wrapper.element.outerHTML).toContain('The changes were not merged');
});
+
+ it('renders merge commit as a link', () => {
+ factory({ state: 'merged', mergeCommitPath: 'https://test.host/merge-commit-link' });
+
+ expect(wrapper.find('[data-testid="merge-commit-sha"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="merge-commit-sha"]').attributes('href')).toBe(
+ 'https://test.host/merge-commit-link',
+ );
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
index 712abfe228a..d519ad2cdb0 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
@@ -39,10 +39,12 @@ describe('Artifacts List', () => {
});
it('renders job url', () => {
- expect(wrapper.findAll(GlLink).at(1).attributes('href')).toEqual(data.artifacts[0].job_path);
+ expect(wrapper.findAllComponents(GlLink).at(1).attributes('href')).toEqual(
+ data.artifacts[0].job_path,
+ );
});
it('renders job name', () => {
- expect(wrapper.findAll(GlLink).at(1).text()).toEqual(data.artifacts[0].job_name);
+ expect(wrapper.findAllComponents(GlLink).at(1).text()).toEqual(data.artifacts[0].job_name);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index 6347e3c3be3..7f0173b7445 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
@@ -4,9 +4,8 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
-import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
-import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import MRWidgetPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { SUCCESS } from '~/vue_merge_request_widget/constants';
import mockData from '../mock_data';
@@ -30,14 +29,13 @@ describe('MRWidgetPipeline', () => {
const findPipelineInfoContainer = () => wrapper.findByTestId('pipeline-info-container');
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineFinishedAt = () => wrapper.findByTestId('finished-at');
- const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
- const findAllPipelineStages = () => wrapper.findAllComponents(PipelineStage);
const findPipelineCoverage = () => wrapper.findByTestId('pipeline-coverage');
const findPipelineCoverageDelta = () => wrapper.findByTestId('pipeline-coverage-delta');
const findPipelineCoverageTooltipText = () =>
wrapper.findByTestId('pipeline-coverage-tooltip').text();
const findPipelineCoverageDeltaTooltipText = () =>
wrapper.findByTestId('pipeline-coverage-delta-tooltip').text();
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -45,7 +43,7 @@ describe('MRWidgetPipeline', () => {
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
- mountFn(PipelineComponent, {
+ mountFn(MRWidgetPipelineComponent, {
propsData: {
...defaultProps,
...props,
@@ -106,8 +104,10 @@ describe('MRWidgetPipeline', () => {
});
it('should render pipeline graph', () => {
+ const stagesCount = mockData.pipeline.details.stages.length;
+
expect(findPipelineMiniGraph().exists()).toBe(true);
- expect(findAllPipelineStages()).toHaveLength(mockData.pipeline.details.stages.length);
+ expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
describe('should render pipeline coverage information', () => {
@@ -176,15 +176,11 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
});
- it('should render pipeline graph with correct styles', () => {
+ it('should render pipeline graph', () => {
const stagesCount = mockData.pipeline.details.stages.length;
expect(findPipelineMiniGraph().exists()).toBe(true);
- expect(findPipelineMiniGraph().findAll('.mr-widget-pipeline-stages')).toHaveLength(
- stagesCount,
- );
-
- expect(findAllPipelineStages()).toHaveLength(stagesCount);
+ expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
it('should render coverage information', () => {
@@ -266,13 +262,13 @@ describe('MRWidgetPipeline', () => {
});
describe('for a detached merge request pipeline', () => {
- it('renders a pipeline widget that reads "Detached merge request pipeline <ID> <status> for <SHA>"', () => {
- pipeline.details.name = 'Detached merge request pipeline';
+ it('renders a pipeline widget that reads "Merge request pipeline <ID> <status> for <SHA>"', () => {
+ pipeline.details.name = 'Merge request pipeline';
pipeline.merge_request_event_type = 'detached';
factory();
- const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const expected = `Merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected);
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index 534c0baf35d..05c259de370 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -110,7 +110,7 @@ describe('Merge request widget rebase component', () => {
expect(findRebaseMessageText()).toContain('Something went wrong!');
});
- describe('Rebase buttons with', () => {
+ describe('Rebase buttons', () => {
beforeEach(() => {
createWrapper(
{
@@ -148,6 +148,79 @@ describe('Merge request widget rebase component', () => {
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
+
+ describe('Rebase when pipelines must succeed is enabled', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ mergeRequestWidgetGraphql,
+ );
+ });
+
+ it('renders only the rebase button', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(false);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ mergeRequestWidgetGraphql,
+ );
+ });
+
+ it('renders both rebase buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
});
describe('without permissions', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
index 11373be578a..530549b7b9c 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
@@ -1,14 +1,16 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
describe('MR widget status icon component', () => {
let wrapper;
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
- const createWrapper = (props, mountFn = shallowMount) => {
- wrapper = mountFn(mrStatusIcon, {
+ const createWrapper = (props) => {
+ wrapper = shallowMount(mrStatusIcon, {
propsData: {
...props,
},
@@ -17,27 +19,45 @@ describe('MR widget status icon component', () => {
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('while loading', () => {
it('renders loading icon', () => {
createWrapper({ status: 'loading' });
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findStatusIcon().exists()).toBe(true);
+ expect(findStatusIcon().props().isLoading).toBe(true);
});
});
describe('with status icon', () => {
it('renders success status icon', () => {
- createWrapper({ status: 'success' }, mount);
+ createWrapper({ status: 'success' });
- expect(wrapper.find('[data-testid="status_success-icon"]').exists()).toBe(true);
+ expect(findStatusIcon().exists()).toBe(true);
+ expect(findStatusIcon().props().iconName).toBe('success');
});
it('renders failed status icon', () => {
- createWrapper({ status: 'failed' }, mount);
+ createWrapper({ status: 'failed' });
- expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true);
+ expect(findStatusIcon().exists()).toBe(true);
+ expect(findStatusIcon().props().iconName).toBe('failed');
+ });
+
+ it('renders merged status icon', () => {
+ createWrapper({ status: 'merged' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props().name).toBe('merge');
+ });
+
+ it('renders closed status icon', () => {
+ createWrapper({ status: 'closed' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props().name).toBe('merge-request-close');
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
index 352bc1a08ea..d6c67dab381 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -128,7 +128,7 @@ describe('MRWidgetSuggestPipeline', () => {
it('emits dismiss upon dismissal button click', () => {
findDismissContainer().vm.$emit('dismiss');
- expect(wrapper.emitted().dismiss).toBeTruthy();
+ expect(wrapper.emitted().dismiss).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index de25e2a0450..635ef0f6b0d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -4,117 +4,171 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<div
class="mr-widget-body media"
>
- <svg
- aria-hidden="true"
- class="gl-text-blue-500 gl-mr-3 gl-mt-1 gl-icon s24"
- data-testid="status_scheduled-icon"
- role="img"
- >
- <use
- href="#status_scheduled"
- />
- </svg>
-
<div
- class="media-body gl-display-flex"
+ class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
>
-
- <h4
- class="gl-mr-3"
- data-testid="statusText"
+ <div
+ class="gl-display-flex gl-m-auto"
>
- Set by
- <a
- class="author-link inline"
+ <div
+ class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2"
>
- <img
- class="avatar avatar-inline s16"
- src="no_avatar.png"
- />
-
- <span
- class="author"
+ <div
+ class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon"
>
-
- </span>
- </a>
- to be merged automatically when the pipeline succeeds
- </h4>
-
+ <div
+ class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto"
+ >
+ <div
+ class="gl-display-flex gl-m-auto gl-translate-y-n50"
+ >
+ <svg
+ aria-label="Scheduled "
+ class="gl-display-block gl-icon s12"
+ data-qa-selector="status_scheduled_icon"
+ data-testid="status-scheduled-icon"
+ role="img"
+ >
+ <use
+ href="#status-scheduled"
+ />
+ </svg>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-w-full"
+ >
<div
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1"
+ class="media-body gl-display-flex"
>
- <div>
- <div
- class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
- lazy=""
- no-caret=""
+
+ <h4
+ class="gl-mr-3"
+ data-testid="statusText"
+ >
+ Set by
+ <a
+ class="author-link inline"
>
- <!---->
+ <img
+ class="avatar avatar-inline s16"
+ src="no_avatar.png"
+ />
+
+ <span
+ class="author"
+ >
+
+ </span>
+ </a>
+ to be merged automatically when the pipeline succeeds
+ </h4>
+
+ <div
+ class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
+ >
+ <div>
+ <div
+ class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
+ lazy=""
+ no-caret=""
+ >
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="dropdown-icon gl-icon s16"
+ data-testid="ellipsis_v-icon"
+ role="img"
+ >
+ <use
+ href="#ellipsis_v"
+ />
+ </svg>
+
+ <span
+ class="gl-new-dropdown-button-text gl-sr-only"
+ >
+
+ </span>
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon dropdown-chevron gl-icon s16"
+ data-testid="chevron-down-icon"
+ role="img"
+ >
+ <use
+ href="#chevron-down"
+ />
+ </svg>
+ </button>
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+
<button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
+ class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
+ data-qa-selector="cancel_auto_merge_button"
+ data-testid="cancelAutomaticMergeButton"
type="button"
>
<!---->
- <svg
- aria-hidden="true"
- class="dropdown-icon gl-icon s16"
- data-testid="ellipsis_v-icon"
- role="img"
- >
- <use
- href="#ellipsis_v"
- />
- </svg>
-
+ <!---->
+
<span
- class="gl-new-dropdown-button-text gl-sr-only"
+ class="gl-button-text"
>
+ Cancel auto-merge
+
</span>
-
- <svg
- aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
- data-testid="chevron-down-icon"
- role="img"
- >
- <use
- href="#chevron-down"
- />
- </svg>
</button>
- <ul
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
</div>
+ </div>
+ </div>
+
+ <div
+ class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
+ >
+ <button
+ class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
+ title="Collapse merge details"
+ type="button"
+ >
+ <!---->
- <button
- class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- type="button"
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="chevron-lg-up-icon"
+ role="img"
>
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Cancel auto-merge
-
- </span>
- </button>
- </div>
+ <use
+ href="#chevron-lg-up"
+ />
+ </svg>
+
+ <!---->
+ </button>
</div>
</div>
</div>
@@ -124,117 +178,171 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<div
class="mr-widget-body media"
>
- <svg
- aria-hidden="true"
- class="gl-text-blue-500 gl-mr-3 gl-mt-1 gl-icon s24"
- data-testid="status_scheduled-icon"
- role="img"
- >
- <use
- href="#status_scheduled"
- />
- </svg>
-
<div
- class="media-body gl-display-flex"
+ class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
>
-
- <h4
- class="gl-mr-3"
- data-testid="statusText"
+ <div
+ class="gl-display-flex gl-m-auto"
>
- Set by
- <a
- class="author-link inline"
+ <div
+ class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2"
>
- <img
- class="avatar avatar-inline s16"
- src="no_avatar.png"
- />
-
- <span
- class="author"
+ <div
+ class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon"
>
-
- </span>
- </a>
- to be merged automatically when the pipeline succeeds
- </h4>
-
+ <div
+ class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto"
+ >
+ <div
+ class="gl-display-flex gl-m-auto gl-translate-y-n50"
+ >
+ <svg
+ aria-label="Scheduled "
+ class="gl-display-block gl-icon s12"
+ data-qa-selector="status_scheduled_icon"
+ data-testid="status-scheduled-icon"
+ role="img"
+ >
+ <use
+ href="#status-scheduled"
+ />
+ </svg>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-w-full"
+ >
<div
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1"
+ class="media-body gl-display-flex"
>
- <div>
- <div
- class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
- lazy=""
- no-caret=""
+
+ <h4
+ class="gl-mr-3"
+ data-testid="statusText"
+ >
+ Set by
+ <a
+ class="author-link inline"
>
- <!---->
+ <img
+ class="avatar avatar-inline s16"
+ src="no_avatar.png"
+ />
+
+ <span
+ class="author"
+ >
+
+ </span>
+ </a>
+ to be merged automatically when the pipeline succeeds
+ </h4>
+
+ <div
+ class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
+ >
+ <div>
+ <div
+ class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
+ lazy=""
+ no-caret=""
+ >
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="dropdown-icon gl-icon s16"
+ data-testid="ellipsis_v-icon"
+ role="img"
+ >
+ <use
+ href="#ellipsis_v"
+ />
+ </svg>
+
+ <span
+ class="gl-new-dropdown-button-text gl-sr-only"
+ >
+
+ </span>
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon dropdown-chevron gl-icon s16"
+ data-testid="chevron-down-icon"
+ role="img"
+ >
+ <use
+ href="#chevron-down"
+ />
+ </svg>
+ </button>
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+
<button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
+ class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
+ data-qa-selector="cancel_auto_merge_button"
+ data-testid="cancelAutomaticMergeButton"
type="button"
>
<!---->
- <svg
- aria-hidden="true"
- class="dropdown-icon gl-icon s16"
- data-testid="ellipsis_v-icon"
- role="img"
- >
- <use
- href="#ellipsis_v"
- />
- </svg>
-
+ <!---->
+
<span
- class="gl-new-dropdown-button-text gl-sr-only"
+ class="gl-button-text"
>
+ Cancel auto-merge
+
</span>
-
- <svg
- aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
- data-testid="chevron-down-icon"
- role="img"
- >
- <use
- href="#chevron-down"
- />
- </svg>
</button>
- <ul
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
</div>
+ </div>
+ </div>
+
+ <div
+ class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
+ >
+ <button
+ class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
+ title="Collapse merge details"
+ type="button"
+ >
+ <!---->
- <button
- class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- type="button"
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="chevron-lg-up-icon"
+ role="img"
>
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Cancel auto-merge
-
- </span>
- </button>
- </div>
+ <use
+ href="#chevron-lg-up"
+ />
+ </svg>
+
+ <!---->
+ </button>
</div>
</div>
</div>
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
deleted file mode 100644
index 7e741bf4660..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PipelineFailed should render error message with a disabled merge button 1`] = `
-<div
- class="mr-widget-body media"
->
- <status-icon-stub
- show-disabled-button="true"
- status="warning"
- />
-
- <div
- class="media-body space-children"
- >
- <span
- class="gl-ml-0! gl-text-body! bold"
- >
- <gl-sprintf-stub
- message="Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}"
- />
- </span>
- </div>
-</div>
-`;
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
index 9332b7e334a..5c07f4ce143 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
@@ -1,25 +1,26 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
describe('MRWidgetArchived', () => {
- let vm;
+ let wrapper;
beforeEach(() => {
- const Component = Vue.extend(archivedComponent);
- vm = mountComponent(Component);
+ wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- it('renders a ci status failed icon', () => {
- expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull();
+ it('renders error icon', () => {
+ expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
+ expect(wrapper.findComponent(StateContainer).props().status).toBe('failed');
});
- it('renders information', () => {
- expect(vm.$el.querySelector('.bold').textContent.trim()).toEqual(
+ it('renders information about merging', () => {
+ expect(wrapper.text()).toContain(
'Merge unavailable: merge requests are read-only on archived projects.',
);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
index 02de426204b..ac18ccf9e26 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
@@ -1,27 +1,25 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
+import { shallowMount } from '@vue/test-utils';
+import CheckingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
describe('MRWidgetChecking', () => {
- let Component;
- let vm;
+ let wrapper;
beforeEach(() => {
- Component = Vue.extend(checkingComponent);
- vm = mountComponent(Component);
+ wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('renders loading icon', () => {
- expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner');
+ expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
+ expect(wrapper.findComponent(StateContainer).props().status).toBe('loading');
});
it('renders information about merging', () => {
- expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual(
- 'Checking if merge request can be merged…',
- );
+ expect(wrapper.text()).toContain('Checking if merge request can be merged…');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
index f7d046eb8f9..06ee017dee7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
@@ -1,39 +1,54 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
+import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+
+const MOCK_DATA = {
+ metrics: {
+ mergedBy: {},
+ closedBy: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ mergedAt: 'Jan 24, 2018 1:02pm UTC',
+ closedAt: 'Jan 24, 2018 1:02pm UTC',
+ readableMergedAt: '',
+ readableClosedAt: 'less than a minute ago',
+ },
+ targetBranchPath: '/twitter/flight/commits/so_long_jquery',
+ targetBranch: 'so_long_jquery',
+};
describe('MRWidgetClosed', () => {
- let vm;
+ let wrapper;
beforeEach(() => {
- const Component = Vue.extend(closedComponent);
- vm = mountComponent(Component, {
- mr: {
- metrics: {
- mergedBy: {},
- closedBy: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://localhost:3000/root',
- avatarUrl:
- 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- },
- mergedAt: 'Jan 24, 2018 1:02pm UTC',
- closedAt: 'Jan 24, 2018 1:02pm UTC',
- readableMergedAt: '',
- readableClosedAt: 'less than a minute ago',
- },
- targetBranchPath: '/twitter/flight/commits/so_long_jquery',
- targetBranch: 'so_long_jquery',
+ wrapper = shallowMount(closedComponent, {
+ propsData: {
+ mr: MOCK_DATA,
},
});
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders closed icon', () => {
+ expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
+ expect(wrapper.findComponent(StateContainer).props().status).toBe('closed');
});
- it('renders warning icon', () => {
- expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
+ it('renders mr widget author time', () => {
+ expect(wrapper.findComponent(MrWidgetAuthorTime).exists()).toBe(true);
+ expect(wrapper.findComponent(MrWidgetAuthorTime).props()).toEqual({
+ actionText: 'Closed by',
+ author: MOCK_DATA.metrics.closedBy,
+ dateTitle: MOCK_DATA.metrics.closedAt,
+ dateReadable: MOCK_DATA.metrics.readableClosedAt,
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 663fabb761c..5d2d1fdd6f1 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -40,7 +40,7 @@ describe('Commits message dropdown component', () => {
wrapper.destroy();
});
- const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
index 989aa76f09b..833fa27d453 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import MrWidgetFailedToMerge from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
describe('MRWidgetFailedToMerge', () => {
@@ -39,7 +40,7 @@ describe('MRWidgetFailedToMerge', () => {
expect(wrapper.vm.intervalId).toBe(dummyIntervalId);
});
- it('clears interval when destroying ', () => {
+ it('clears interval when destroying', () => {
createComponent();
wrapper.destroy();
@@ -128,7 +129,11 @@ describe('MRWidgetFailedToMerge', () => {
await nextTick();
- expect(wrapper.find('.js-refresh-label').text().trim()).toBe('Refreshing now');
+ const stateContainerWrapper = wrapper.findComponent(StateContainer);
+
+ expect(stateContainerWrapper.exists()).toBe(true);
+ expect(stateContainerWrapper.props('status')).toBe('loading');
+ expect(stateContainerWrapper.text().trim()).toBe('Refreshing now');
});
});
@@ -146,9 +151,9 @@ describe('MRWidgetFailedToMerge', () => {
});
it('renders refresh button', () => {
- expect(
- wrapper.find('[data-testid="merge-request-failed-refresh-button"]').text().trim(),
- ).toBe('Refresh now');
+ expect(wrapper.findComponent(StateContainer).props('actions')).toMatchObject([
+ { text: 'Refresh now', onClick: expect.any(Function) },
+ ]);
});
it('renders remaining time', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
index 63e93074857..c6e7198c678 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
@@ -1,25 +1,27 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('MRWidgetNotAllowed', () => {
- let vm;
+ let wrapper;
+
beforeEach(() => {
- const Component = Vue.extend(notAllowedComponent);
- vm = mountComponent(Component);
+ wrapper = shallowMount(notAllowedComponent);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('renders success icon', () => {
- expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null);
+ expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(StatusIcon).props().status).toBe('success');
});
it('renders informative text', () => {
- expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
- expect(vm.$el.innerText).toContain(
+ expect(wrapper.text()).toContain('Ready to be merged automatically.');
+ expect(wrapper.text()).toContain(
'Ask someone with write access to this repository to merge this request',
);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 9b10b078e89..4219ad70b4c 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,26 +1,25 @@
-import { shallowMount, mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('MRWidgetPipelineBlocked', () => {
let wrapper;
- const createWrapper = (mountFn = shallowMount) => {
- wrapper = mountFn(PipelineBlockedComponent);
- };
+ beforeEach(() => {
+ wrapper = shallowMount(PipelineBlockedComponent);
+ });
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
- it('renders warning icon', () => {
- createWrapper(mount);
-
- expect(wrapper.find('.ci-status-icon-warning').exists()).toBe(true);
+ it('renders error icon', () => {
+ expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
});
it('renders information text', () => {
- createWrapper();
-
expect(wrapper.text()).toBe(
"Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
);
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
index 4e44ac539f2..d5619d4996d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,11 +1,17 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('PipelineFailed', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMount(PipelineFailed);
+ wrapper = shallowMount(PipelineFailed, {
+ stubs: {
+ GlSprintf,
+ },
+ });
};
beforeEach(() => {
@@ -17,7 +23,14 @@ describe('PipelineFailed', () => {
wrapper = null;
});
+ it('should render error status icon', () => {
+ expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
+ });
+
it('should render error message with a disabled merge button', () => {
- expect(wrapper.element).toMatchSnapshot();
+ expect(wrapper.text()).toContain('Merge blocked: pipeline must succeed.');
+ expect(wrapper.text()).toContain('Push a commit that fixes the failure');
+ expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 6e89cd41559..9a6bf66909e 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -111,7 +111,7 @@ const createComponent = (
};
const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
-const findCommitEditElements = () => wrapper.findAll(CommitEdit);
+const findCommitEditElements = () => wrapper.findAllComponents(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
const findTipLink = () => wrapper.find(GlSprintf);
@@ -549,7 +549,7 @@ describe('ReadyToMerge', () => {
${'squashIsSelected'} | ${'selected'} | ${'value'} | ${false}
${'squashIsSelected'} | ${'unselected'} | ${'value'} | ${false}
`(
- 'is $state when squashIsReadonly returns $expectation ',
+ 'is $state when squashIsReadonly returns $expectation',
({ squashState, prop, expectation }) => {
createComponent({
mr: { commitsCount: 2, enableSquashBeforeMerge: true, [squashState]: expectation },
diff --git a/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
index 8f20d6a8fc9..7a868eb8cc9 100644
--- a/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -16,7 +16,8 @@ describe('MrWidgetTerraformConainer', () => {
const propsData = { endpoint: '/path/to/terraform/report.json' };
const findHeader = () => wrapper.find('[data-testid="terraform-header-text"]');
- const findPlans = () => wrapper.findAll(TerraformPlan).wrappers.map((x) => x.props('plan'));
+ const findPlans = () =>
+ wrapper.findAllComponents(TerraformPlan).wrappers.map((x) => x.props('plan'));
const mockPollingApi = (response, body, header) => {
mock.onGet(propsData.endpoint).reply(response, body, header);
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
index 6bb718082a4..8dbee9b370c 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
@@ -12,8 +12,8 @@ describe('MR Widget App', () => {
});
};
- it('mounts the component', () => {
+ it('does not mount if widgets array is empty', () => {
createComponent();
- expect(wrapper.findByTestId('mr-widget-app').exists()).toBe(true);
+ expect(wrapper.findByTestId('mr-widget-app').exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js
new file mode 100644
index 00000000000..c2128d3ff33
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js
@@ -0,0 +1,39 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WidgetContentSection from '~/vue_merge_request_widget/components/widget/widget_content_section.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+
+describe('~/vue_merge_request_widget/components/widget/widget_content_section.vue', () => {
+ let wrapper;
+
+ const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+
+ const createComponent = ({ propsData, slots } = {}) => {
+ wrapper = shallowMountExtended(WidgetContentSection, {
+ propsData: {
+ widgetName: 'MyWidget',
+ ...propsData,
+ },
+ slots,
+ });
+ };
+
+ it('does not render the status icon when it is not provided', () => {
+ createComponent();
+ expect(findStatusIcon().exists()).toBe(false);
+ });
+
+ it('renders the status icon when provided', () => {
+ createComponent({ propsData: { statusIconName: 'failed' } });
+ expect(findStatusIcon().exists()).toBe(true);
+ });
+
+ it('renders the default slot', () => {
+ createComponent({
+ slots: {
+ default: 'Hello world',
+ },
+ });
+
+ expect(wrapper.findByText('Hello world').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 3c08ffdef18..b67b5703ad5 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -3,16 +3,21 @@ import * as Sentry from '@sentry/browser';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
describe('MR Widget', () => {
let wrapper;
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+ const findExpandedSection = () => wrapper.findByTestId('widget-extension-collapsed-section');
+ const findActionButtons = () => wrapper.findComponent(ActionButtons);
+ const findToggleButton = () => wrapper.findByTestId('toggle-button');
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(Widget, {
propsData: {
+ isCollapsible: false,
loadingText: 'Loading widget',
widgetName: 'MyWidget',
value: {
@@ -38,14 +43,15 @@ describe('MR Widget', () => {
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises();
expect(fetchCollapsedData).toHaveBeenCalled();
- expect(wrapper.vm.error).toBe(null);
+ expect(wrapper.vm.summaryError).toBe(null);
});
it('sets the error text when fetch method fails', async () => {
const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises();
- expect(wrapper.vm.error).toBe('Failed to load');
+ expect(wrapper.findByText('Failed to load').exists()).toBe(true);
+ expect(findStatusIcon().props()).toMatchObject({ iconName: 'failed', isLoading: false });
});
it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {
@@ -111,7 +117,7 @@ describe('MR Widget', () => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
createComponent({
propsData: {
- fetchCollapsedData: async () => Promise.reject(error),
+ fetchCollapsedData: () => Promise.reject(error),
},
});
await waitForPromises();
@@ -125,7 +131,7 @@ describe('MR Widget', () => {
createComponent({
propsData: {
summary: 'Hello world',
- fetchCollapsedData: async () => Promise.resolve(),
+ fetchCollapsedData: () => Promise.resolve(),
},
});
@@ -137,7 +143,7 @@ describe('MR Widget', () => {
it('displays the summary slot when provided', () => {
createComponent({
propsData: {
- fetchCollapsedData: async () => Promise.resolve(),
+ fetchCollapsedData: () => Promise.resolve(),
},
slots: {
summary: '<b>More complex summary</b>',
@@ -149,19 +155,167 @@ describe('MR Widget', () => {
);
});
- it('displays the content slot when provided', () => {
+ it('does not display action buttons if actionButtons is not provided', () => {
createComponent({
propsData: {
- fetchCollapsedData: async () => Promise.resolve(),
+ fetchCollapsedData: () => Promise.resolve(),
+ },
+ });
+
+ expect(findActionButtons().exists()).toBe(false);
+ });
+
+ it('does display action buttons if actionButtons is provided', () => {
+ const actionButtons = [{ text: 'click-me', href: '#' }];
+
+ createComponent({
+ propsData: {
+ fetchCollapsedData: () => Promise.resolve(),
+ actionButtons,
+ },
+ });
+
+ expect(findActionButtons().props('tertiaryButtons')).toEqual(actionButtons);
+ });
+ });
+
+ describe('handle collapse toggle', () => {
+ it('displays the toggle button correctly', () => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ fetchCollapsedData: () => Promise.resolve(),
},
slots: {
content: '<b>More complex content</b>',
},
});
- expect(wrapper.findByTestId('widget-extension-collapsed-section').text()).toBe(
- 'More complex content',
- );
+ expect(findToggleButton().attributes('title')).toBe('Show details');
+ expect(findToggleButton().attributes('aria-label')).toBe('Show details');
+ });
+
+ it('does not display the content slot until toggle is clicked', async () => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ fetchCollapsedData: () => Promise.resolve(),
+ },
+ slots: {
+ content: '<b>More complex content</b>',
+ },
+ });
+
+ expect(findExpandedSection().exists()).toBe(false);
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+ expect(findExpandedSection().text()).toBe('More complex content');
+ });
+
+ it('does not display the toggle button if isCollapsible is false', () => {
+ createComponent({
+ propsData: {
+ isCollapsible: false,
+ fetchCollapsedData: () => Promise.resolve(),
+ },
+ });
+
+ expect(findToggleButton().exists()).toBe(false);
+ });
+
+ it('fetches expanded data when clicked for the first time', async () => {
+ const mockDataCollapsed = {
+ headers: {},
+ status: 200,
+ data: { vulnerabilities: [{ vuln: 1 }] },
+ };
+
+ const mockDataExpanded = {
+ headers: {},
+ status: 200,
+ data: { vulnerabilities: [{ vuln: 2 }] },
+ };
+
+ const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded);
+
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
+ fetchExpandedData,
+ },
+ });
+
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+
+ // First fetches the collapsed data
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ collapsed: mockDataCollapsed.data,
+ expanded: null,
+ });
+
+ // Then fetches the expanded data
+ expect(wrapper.emitted('input')[1][0]).toEqual({
+ collapsed: null,
+ expanded: mockDataExpanded.data,
+ });
+
+ // Triggering a click does not call the expanded data again
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+ expect(fetchExpandedData).toHaveBeenCalledTimes(1);
+ });
+
+ it('allows refetching when fetch expanded data returns an error', async () => {
+ const fetchExpandedData = jest.fn().mockRejectedValue({ error: true });
+
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ fetchCollapsedData: () => Promise.resolve([]),
+ fetchExpandedData,
+ },
+ });
+
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+
+ // First fetches the collapsed data
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ collapsed: undefined,
+ expanded: null,
+ });
+
+ expect(fetchExpandedData).toHaveBeenCalledTimes(1);
+ expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed
+
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+ expect(fetchExpandedData).toHaveBeenCalledTimes(2);
+ });
+
+ it('resets the error message when another request is fetched', async () => {
+ const fetchExpandedData = jest.fn().mockRejectedValue({ error: true });
+
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ fetchCollapsedData: () => Promise.resolve([]),
+ fetchExpandedData,
+ },
+ });
+
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.findByText('Failed to load').exists()).toBe(true);
+ fetchExpandedData.mockImplementation(() => new Promise(() => {}));
+
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(wrapper.findByText('Failed to load').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
index a285d26f404..a8912405fa8 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
@@ -189,7 +189,7 @@ describe('DeploymentAction component', () => {
});
});
- describe('it should call the executeAction method ', () => {
+ describe('it should call the executeAction method', () => {
beforeEach(async () => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index 5c1d3c8e8e8..82743275739 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
@@ -15,6 +15,7 @@ import { failedReport } from 'jest/reports/mock_data/mock_data';
import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json';
import newErrorsTestReports from 'jest/reports/mock_data/new_errors_report.json';
import newFailedTestReports from 'jest/reports/mock_data/new_failures_report.json';
+import newFailedTestWithNullFilesReport from 'jest/reports/mock_data/new_failures_with_null_files_report.json';
import successTestReports from 'jest/reports/mock_data/no_failures_report.json';
import resolvedFailures from 'jest/reports/mock_data/resolved_failures.json';
import recentFailures from 'jest/reports/mock_data/recent_failures_report.json';
@@ -157,6 +158,15 @@ describe('Test report extension', () => {
);
});
+ it('hides copy failed tests button when endpoint returns null files', async () => {
+ mockApi(httpStatusCodes.OK, newFailedTestWithNullFilesReport);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findCopyFailedSpecsBtn().exists()).toBe(false);
+ });
+
it('copy failed tests button updates tooltip text when clicked', async () => {
mockApi(httpStatusCodes.OK, newFailedTestReports);
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 819841317f9..cc894f94f80 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -845,7 +845,7 @@ describe('MrWidgetOptions', () => {
${'closed'} | ${false} | ${'hides'}
${'merged'} | ${true} | ${'shows'}
${'open'} | ${true} | ${'shows'}
- `('it $showText merge error when state is $state', ({ state, show }) => {
+ `('$showText merge error when state is $state', ({ state, show }) => {
createComponent({ ...mockData, state, merge_error: 'Error!' });
expect(wrapper.find('[data-testid="merge_error"]').exists()).toBe(show);
@@ -1133,7 +1133,7 @@ describe('MrWidgetOptions', () => {
widgetName | nonStandardEvent
${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
- ${'WidgetIssues'} | ${'i_testing_load_performance_widget_total'}
+ ${'WidgetIssues'} | ${'i_testing_issues_widget_total'}
${'WidgetTestReport'} | ${'i_testing_summary_widget_total'}
`(
"sends non-standard events for the '$widgetName' widget",
diff --git a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
index 22562bb4ddb..1a109aad911 100644
--- a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
@@ -60,7 +60,7 @@ describe('Artifacts App Store Actions', () => {
});
describe('success', () => {
- it('dispatches requestArtifacts and receiveArtifactsSuccess ', () => {
+ it('dispatches requestArtifacts and receiveArtifactsSuccess', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
{
text: 'result.txt',
@@ -103,7 +103,7 @@ describe('Artifacts App Store Actions', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
- it('dispatches requestArtifacts and receiveArtifactsError ', () => {
+ it('dispatches requestArtifacts and receiveArtifactsError', () => {
return testAction(
fetchArtifacts,
null,
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 59e21b2ff40..d309432bc63 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -248,7 +248,7 @@ describe('AlertDetails', () => {
});
});
- it('shows error alert when incident creation fails ', async () => {
+ it('shows error alert when incident creation fails', async () => {
const errorMsg = 'Something went wrong';
mountComponent({
mountMethod: mount,
diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
index cf04c1eb24a..9d84a535d67 100644
--- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
@@ -42,7 +42,7 @@ describe('Alert Metrics', () => {
});
describe('Empty state', () => {
- it('should display a message when metrics dashboard url is not provided ', () => {
+ it('should display a message when metrics dashboard url is not provided', () => {
mountComponent();
expect(findChart().exists()).toBe(false);
expect(findEmptyState().text()).toBe("Metrics weren't available in the alerts payload.");
diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
deleted file mode 100644
index 7f655d67ae8..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
+++ /dev/null
@@ -1,26 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Code Block with default props renders correctly 1`] = `
-<pre
- class="code-block rounded code"
->
- <code
- class="d-block"
- >
- test-code
- </code>
-</pre>
-`;
-
-exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
-<pre
- class="code-block rounded code"
- style="max-height: 200px; overflow-y: auto;"
->
- <code
- class="d-block"
- >
- test-code
- </code>
-</pre>
-`;
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index a943d931f67..27b6718fb8e 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,6 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
describe('CI Badge Link Component', () => {
let wrapper;
@@ -79,17 +84,20 @@ describe('CI Badge Link Component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
+ it.each(Object.keys(statuses))('should render badge for status: %s', async (status) => {
createComponent({ status: statuses[status] });
- expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
+ expect(wrapper.attributes('href')).toBe();
expect(wrapper.text()).toBe(statuses[status].text);
expect(wrapper.classes()).toContain('ci-status');
expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
expect(findIcon().exists()).toBe(true);
+
+ await wrapper.trigger('click');
+
+ expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path);
});
it('should not render label', () => {
@@ -97,4 +105,12 @@ describe('CI Badge Link Component', () => {
expect(wrapper.text()).toBe('');
});
+
+ it('should emit ciStatusBadgeClick event', async () => {
+ createComponent({ status: statuses.success });
+
+ await wrapper.trigger('click');
+
+ expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
+ });
});
diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
new file mode 100644
index 00000000000..181692e61b5
--- /dev/null
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -0,0 +1,65 @@
+import { shallowMount } from '@vue/test-utils';
+import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('Code Block Highlighted', () => {
+ let wrapper;
+
+ const code = 'const foo = 1;';
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(CodeBlock, { propsData });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders highlighted code if language is supported', async () => {
+ createComponent({ code, language: 'javascript' });
+
+ await waitForPromises();
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <code-block-stub
+ class="highlight"
+ code=""
+ maxheight="initial"
+ >
+ <span>
+ <span
+ class="hljs-keyword"
+ >
+ const
+ </span>
+ foo =
+ <span
+ class="hljs-number"
+ >
+ 1
+ </span>
+ ;
+ </span>
+ </code-block-stub>
+ `);
+ });
+
+ it("renders plain text if language isn't supported", async () => {
+ createComponent({ code, language: 'foobar' });
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]);
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <code-block-stub
+ class="highlight"
+ code=""
+ maxheight="initial"
+ >
+ <span>
+ const foo = 1;
+ </span>
+ </code-block-stub>
+ `);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 60b0b0b566b..9a4dbcc47ff 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
describe('Code Block', () => {
let wrapper;
- const defaultProps = {
- code: 'test-code',
- };
+ const code = 'test-code';
- const createComponent = (props = {}) => {
+ const createComponent = (propsData, slots = {}) => {
wrapper = shallowMount(CodeBlock, {
- propsData: {
- ...defaultProps,
- ...props,
- },
+ slots,
+ propsData,
});
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- describe('with default props', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('overwrites the default slot', () => {
+ createComponent({}, { default: 'DEFAULT SLOT' });
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ >
+ DEFAULT SLOT
+ </pre>
+ `);
});
- describe('with maxHeight set to "200px"', () => {
- beforeEach(() => {
- createComponent({ maxHeight: '200px' });
- });
+ it('renders with empty code prop', () => {
+ createComponent({});
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ >
+ <code
+ class="d-block"
+ >
+
+ </code>
+ </pre>
+ `);
+ });
+
+ it('renders code prop when provided', () => {
+ createComponent({ code });
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ >
+ <code
+ class="d-block"
+ >
+ test-code
+ </code>
+ </pre>
+ `);
+ });
+
+ it('sets maxHeight properly when provided', () => {
+ createComponent({ code, maxHeight: '200px' });
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ style="max-height: 200px; overflow-y: auto;"
+ >
+ <code
+ class="d-block"
+ >
+ test-code
+ </code>
+ </pre>
+ `);
});
});
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 04f63b4bd45..68684004b82 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -66,7 +66,7 @@ describe('Diff Stats Dropdown', () => {
createComponent({ files: mockFiles });
});
- it('when no file name provided ', () => {
+ it('when no file name provided', () => {
expect(findChangedFiles().at(0).text()).toContain(i18n.noFileNameAvailable);
});
@@ -153,7 +153,7 @@ describe('Diff Stats Dropdown', () => {
createComponent({ files: mockFiles });
});
- it('updates the URL ', () => {
+ it('updates the URL', () => {
findChangedFiles().at(0).vm.$emit('click');
expect(window.location.hash).toBe(mockFiles[0].href);
findChangedFiles().at(1).vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index 2dcd91f737f..6dc018797a6 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -157,13 +157,13 @@ describe('GlModalVuex', () => {
const handler = modalFooterSlotContent.mock.calls[0][0][handlerName];
- expect(wrapper.emitted(handlerName)).toBeFalsy();
+ expect(wrapper.emitted(handlerName)).toBeUndefined();
expect(actions.hide).not.toHaveBeenCalled();
handler();
expect(actions.hide).toHaveBeenCalledTimes(1);
- expect(wrapper.emitted(handlerName)).toBeTruthy();
+ expect(wrapper.emitted(handlerName)).toHaveLength(1);
},
);
});
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index 9f819cc4e94..ae9c920ebd2 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -49,7 +49,7 @@ describe('Pagination links component', () => {
});
describe('rendering', () => {
- it('it renders the gl-paginated-list', () => {
+ it('renders the gl-paginated-list', () => {
expect(wrapper.find('ul.list-group').exists()).toBe(true);
expect(wrapper.findAll('li.list-group-item').length).toBe(2);
});
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index 70f4693ae81..fa7fabfaef6 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -108,7 +108,7 @@ describe('Registry Search', () => {
]);
});
- it('on sort item click emits sorting:changed event ', () => {
+ it('on sort item click emits sorting:changed event', () => {
mountComponent();
findSortingItems().at(1).vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index afad9314ace..48530a0261f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -56,7 +56,7 @@ export const mockSuggestedColors = {
'#013220': 'Dark green',
'#6699cc': 'Blue-gray',
'#0000ff': 'Blue',
- '#e6e6fa': 'Lavendar',
+ '#e6e6fa': 'Lavender',
'#9400d3': 'Dark violet',
'#330066': 'Deep violet',
'#808080': 'Gray',
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 4fbc907a813..e020d9a557e 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -110,6 +110,13 @@ describe('Source Viewer component', () => {
expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
});
+ it('correctly maps languages starting with uppercase', async () => {
+ await createComponent({ language: 'Python3' });
+ const languageDefinition = await import(`highlight.js/lib/languages/python`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('python', languageDefinition.default);
+ });
+
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
});
@@ -149,7 +156,7 @@ describe('Source Viewer component', () => {
it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toBeCalledWith('showBlobInteractionZones', path);
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
describe('LineHighlighter', () => {
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index 1798ca5ccde..f9d615d4f68 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -5,7 +5,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
@@ -86,7 +86,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
@@ -171,7 +171,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
@@ -256,7 +256,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
@@ -342,7 +342,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
@@ -428,7 +428,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
@@ -514,7 +514,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
type="button"
>
<div
diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
index 70dec42ab32..521744154ba 100644
--- a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
+++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
@@ -84,7 +84,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', () => {
- expect(defaultScopedSlotSpy).lastCalledWith(initialSlotProps());
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(initialSlotProps());
});
});
@@ -98,7 +98,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isDismissed: true,
isLoadingQuery: false,
@@ -117,7 +117,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isLoadingQuery: false,
shouldShowCallout: true,
@@ -136,7 +136,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isLoadingQuery: false,
queryError: expect.any(Error),
@@ -155,7 +155,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isAnonUser: true,
isLoadingQuery: false,
@@ -186,7 +186,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isLoadingQuery: false,
shouldShowCallout: true,
@@ -217,7 +217,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', async () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isLoadingQuery: false,
shouldShowCallout: true,
@@ -229,7 +229,7 @@ describe('UserCalloutDismisser', () => {
// Wait for Vue re-render due to prop change
await nextTick();
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isDismissed: true,
isLoadingMutation: true,
@@ -240,7 +240,7 @@ describe('UserCalloutDismisser', () => {
// Wait for mutation to resolve
await waitForPromises();
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isDismissed: true,
isLoadingQuery: false,
@@ -270,7 +270,7 @@ describe('UserCalloutDismisser', () => {
});
it('passes expected slot props to child', async () => {
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isLoadingQuery: false,
shouldShowCallout: true,
@@ -282,7 +282,7 @@ describe('UserCalloutDismisser', () => {
// Wait for Vue re-render due to prop change
await nextTick();
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isDismissed: true,
isLoadingMutation: true,
@@ -293,7 +293,7 @@ describe('UserCalloutDismisser', () => {
// Wait for mutation to resolve
await waitForPromises();
- expect(defaultScopedSlotSpy).lastCalledWith(
+ expect(defaultScopedSlotSpy).toHaveBeenLastCalledWith(
initialSlotProps({
isDismissed: true,
isLoadingQuery: false,
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index b7ce3e47cef..6d48000beb0 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,8 +1,15 @@
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import {
+ I18N_USER_BLOCKED,
+ I18N_USER_LEARN,
+ I18N_USER_FOLLOW,
+ I18N_USER_UNFOLLOW,
+} from '~/vue_shared/components/user_popover/constants';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/api/user_api';
@@ -310,7 +317,9 @@ describe('User Popover Component', () => {
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(true);
expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl);
- expect(securityBotDocsLink.text()).toBe('Learn more about GitLab Security Bot');
+ expect(securityBotDocsLink.text()).toBe(
+ sprintf(I18N_USER_LEARN, { name: SECURITY_BOT_USER.name }),
+ );
});
it("does not show a link to the bot's documentation if there is no website_url", () => {
@@ -320,9 +329,10 @@ describe('User Popover Component', () => {
});
it("doesn't escape user's name", () => {
- createWrapper({ user: { ...SECURITY_BOT_USER, name: '%<>\';"' } });
+ const name = '%<>\';"';
+ createWrapper({ user: { ...SECURITY_BOT_USER, name } });
const securityBotDocsLink = findSecurityBotDocsLink();
- expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
+ expect(securityBotDocsLink.text()).toBe(sprintf(I18N_USER_LEARN, { name }, false));
});
it('does not display local time', () => {
@@ -336,7 +346,7 @@ describe('User Popover Component', () => {
beforeEach(() => createWrapper());
it('renders the Follow button with the correct variant', () => {
- expect(findToggleFollowButton().text()).toBe('Follow');
+ expect(findToggleFollowButton().text()).toBe(I18N_USER_FOLLOW);
expect(findToggleFollowButton().props('variant')).toBe('confirm');
});
@@ -387,7 +397,7 @@ describe('User Popover Component', () => {
beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } }));
it('renders the Unfollow button with the correct variant', () => {
- expect(findToggleFollowButton().text()).toBe('Unfollow');
+ expect(findToggleFollowButton().text()).toBe(I18N_USER_UNFOLLOW);
expect(findToggleFollowButton().props('variant')).toBe('default');
});
@@ -441,6 +451,25 @@ describe('User Popover Component', () => {
});
});
+ describe('when the user is blocked', () => {
+ const bio = 'My super interesting bio';
+ const status = 'My status';
+ beforeEach(() =>
+ createWrapper({
+ user: { ...DEFAULT_PROPS.user, state: 'blocked', bio, status: { message_html: status } },
+ }),
+ );
+
+ it('renders warning', () => {
+ expect(wrapper.text()).toContain(I18N_USER_BLOCKED);
+ });
+
+ it("doesn't show other information", () => {
+ expect(wrapper.text()).not.toContain(bio);
+ expect(wrapper.text()).not.toContain(status);
+ });
+ });
+
describe('when API does not support `isFollowed`', () => {
beforeEach(() => {
const user = {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index d843da4da5b..e5594b6d37e 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -164,8 +164,7 @@ describe('IssuableEditForm', () => {
const titleInputEl = wrapper.findComponent(GlFormInput);
titleInputEl.vm.$emit('keydown', eventObj, 'title');
-
- expect(wrapper.emitted('keydown-title')).toBeTruthy();
+ expect(wrapper.emitted('keydown-title')).toHaveLength(1);
expect(wrapper.emitted('keydown-title')[0]).toMatchObject([
eventObj,
{
@@ -179,8 +178,7 @@ describe('IssuableEditForm', () => {
const descriptionInputEl = wrapper.find('[data-testid="description"] textarea');
descriptionInputEl.trigger('keydown', eventObj, 'description');
-
- expect(wrapper.emitted('keydown-description')).toBeTruthy();
+ expect(wrapper.emitted('keydown-description')).toHaveLength(1);
expect(wrapper.emitted('keydown-description')[0]).toMatchObject([
eventObj,
{
diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
index 39909e26ef0..0a5e46d9263 100644
--- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
@@ -93,7 +93,7 @@ describe('ManageViaMr component', () => {
createComponent({ apolloProvider, featureName, featureType, isFeatureConfigured: true });
});
- it('it does not render a button', () => {
+ it('does not render a button', () => {
expect(findButton().exists()).toBe(false);
});
});
@@ -104,7 +104,7 @@ describe('ManageViaMr component', () => {
createComponent({ apolloProvider, featureName, featureType, isFeatureConfigured: false });
});
- it('it does render a button', () => {
+ it('does render a button', () => {
expect(findButton().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index de20369eb1b..13e04ef6671 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -49,6 +49,6 @@ describe('ItemTitle', () => {
findInputEl().element.innerText = mockUpdatedTitle;
await findInputEl().trigger(sourceEvent);
- expect(wrapper.emitted(eventName)).toBeTruthy();
+ expect(wrapper.emitted(eventName)).toBeDefined();
});
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index a1f1d47ab90..3c312fb4552 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,15 +1,30 @@
-import { GlModal } from '@gitlab/ui';
+import { GlDropdownDivider, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
+const TEST_ID_DELETE_ACTION = 'delete-action';
+
describe('WorkItemActions component', () => {
let wrapper;
let glModalDirective;
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
- wrapper.findByTestId('confidentiality-toggle-action');
- const findDeleteButton = () => wrapper.findByTestId('delete-action');
+ wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
+ const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
+ const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
+ const findDropdownItemsActual = () =>
+ findDropdownItems().wrappers.map((x) => {
+ if (x.is(GlDropdownDivider)) {
+ return { divider: true };
+ }
+
+ return {
+ testId: x.attributes('data-testid'),
+ text: x.text(),
+ };
+ });
const createComponent = ({
canUpdate = true,
@@ -19,7 +34,14 @@ describe('WorkItemActions component', () => {
} = {}) => {
glModalDirective = jest.fn();
wrapper = shallowMountExtended(WorkItemActions, {
- propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential },
+ propsData: {
+ workItemId: '123',
+ canUpdate,
+ canDelete,
+ isConfidential,
+ isParentConfidential,
+ workItemType: 'Task',
+ },
directives: {
glModal: {
bind(_, { value }) {
@@ -44,8 +66,19 @@ describe('WorkItemActions component', () => {
it('renders dropdown actions', () => {
createComponent();
- expect(findConfidentialityToggleButton().exists()).toBe(true);
- expect(findDeleteButton().exists()).toBe(true);
+ expect(findDropdownItemsActual()).toEqual([
+ {
+ testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
+ text: 'Turn on confidentiality',
+ },
+ {
+ divider: true,
+ },
+ {
+ testId: TEST_ID_DELETE_ACTION,
+ text: 'Delete task',
+ },
+ ]);
});
describe('toggle confidentiality action', () => {
@@ -103,7 +136,8 @@ describe('WorkItemActions component', () => {
canDelete: false,
});
- expect(wrapper.findByTestId('delete-action').exists()).toBe(false);
+ expect(findDeleteButton().exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index f0ef8aee7a9..28231fad108 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlTokenSelector, GlSkeletonLoader } from '@gitlab/ui';
+import { GlLink, GlTokenSelector, GlSkeletonLoader, GlIntersectionObserver } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,12 +8,17 @@ import { mockTracking } from 'helpers/tracking_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
+import { temporaryConfig } from '~/graphql_shared/issuable_client';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
-import { i18n, TASK_TYPE_NAME, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import { temporaryConfig } from '~/work_items/graphql/provider';
+import {
+ i18n,
+ TASK_TYPE_NAME,
+ TRACKING_CATEGORY_SHOW,
+ DEFAULT_PAGE_SIZE_ASSIGNEES,
+} from '~/work_items/constants';
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
@@ -22,6 +27,8 @@ import {
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
updateWorkItemMutationResponse,
+ projectMembersResponseWithCurrentUserWithNextPage,
+ projectMembersResponseWithNoMatchingUsers,
} from '../mock_data';
Vue.use(VueApollo);
@@ -40,15 +47,25 @@ describe('WorkItemAssignees component', () => {
const findEmptyState = () => wrapper.findByTestId('empty-state');
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
const findAssigneesTitle = () => wrapper.findByTestId('assignees-title');
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+
+ const triggerInfiniteScroll = () =>
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
const successSearchQueryHandler = jest
.fn()
.mockResolvedValue(projectMembersResponseWithCurrentUser);
+ const successSearchQueryHandlerWithMoreAssignees = jest
+ .fn()
+ .mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage);
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
+ const successSearchWithNoMatchingUsers = jest
+ .fn()
+ .mockResolvedValue(projectMembersResponseWithNoMatchingUsers);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
@@ -82,9 +99,6 @@ describe('WorkItemAssignees component', () => {
});
wrapper = mountExtended(WorkItemAssignees, {
- provide: {
- fullPath: 'test-project-path',
- },
propsData: {
assignees,
workItemId,
@@ -92,6 +106,7 @@ describe('WorkItemAssignees component', () => {
workItemType: TASK_TYPE_NAME,
canUpdate,
canInviteMembers,
+ fullPath: 'test-project-path',
},
attachTo: document.body,
apolloProvider,
@@ -459,4 +474,56 @@ describe('WorkItemAssignees component', () => {
expect(findInviteMembersTrigger().exists()).toBe(true);
});
});
+
+ describe('load more assignees', () => {
+ it('does not have intersection observer when no matching users', async () => {
+ createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers });
+ findTokenSelector().vm.$emit('focus');
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+
+ it('does not trigger load more when does not have next page', async () => {
+ createComponent();
+ findTokenSelector().vm.$emit('focus');
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+
+ it('triggers load more when there are more users', async () => {
+ createComponent({ searchQueryHandler: successSearchQueryHandlerWithMoreAssignees });
+ findTokenSelector().vm.$emit('focus');
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findIntersectionObserver().exists()).toBe(true);
+
+ triggerInfiniteScroll();
+
+ expect(successSearchQueryHandlerWithMoreAssignees).toHaveBeenCalledWith({
+ first: DEFAULT_PAGE_SIZE_ASSIGNEES,
+ after:
+ projectMembersResponseWithCurrentUserWithNextPage.data.workspace.users.pageInfo.endCursor,
+ search: '',
+ fullPath: 'test-project-path',
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 8017c46dea8..d3165d8dc26 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -10,9 +10,9 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import updateWorkItemWidgetsMutation from '~/work_items/graphql/update_work_item_widgets.mutation.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
- updateWorkItemWidgetsResponse,
+ updateWorkItemMutationResponse,
workItemResponseFactory,
workItemQueryResponse,
} from '../mock_data';
@@ -31,7 +31,7 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
- const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse);
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
@@ -53,13 +53,11 @@ describe('WorkItemDescription', () => {
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemQuery, workItemResponseHandler],
- [updateWorkItemWidgetsMutation, mutationHandler],
+ [updateWorkItemMutation, mutationHandler],
]),
propsData: {
workItemId: id,
- },
- provide: {
- fullPath: '/group/project',
+ fullPath: 'test-project-path',
},
stubs: {
MarkdownField,
@@ -175,7 +173,7 @@ describe('WorkItemDescription', () => {
isEditing: true,
mutationHandler: jest.fn().mockResolvedValue({
data: {
- workItemUpdateWidgets: {
+ workItemUpdate: {
workItem: {},
errors: [error],
},
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 01891012f99..6b1ef8971d3 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -113,7 +113,7 @@ describe('WorkItemDetailModal component', () => {
createComponent();
findModal().vm.$emit('hide');
- expect(wrapper.emitted('close')).toBeTruthy();
+ expect(wrapper.emitted('close')).toHaveLength(1);
});
it('hides the modal when WorkItemDetail emits `close` event', () => {
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 823981df880..b047e0dc8d7 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -2,29 +2,33 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gi
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
+import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
-import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
-import { temporaryConfig } from '~/work_items/graphql/provider';
+import { temporaryConfig } from '~/graphql_shared/issuable_client';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
- workItemTitleSubscriptionResponse,
- workItemResponseFactory,
mockParent,
+ workItemDatesSubscriptionResponse,
+ workItemResponseFactory,
+ workItemTitleSubscriptionResponse,
+ workItemWeightSubscriptionResponse,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -40,7 +44,9 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
+ const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
@@ -49,9 +55,9 @@ describe('WorkItemDetail component', () => {
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
+ const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
- const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight);
const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
const findParentButton = () => findParent().findComponent(GlButton);
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
@@ -64,19 +70,26 @@ describe('WorkItemDetail component', () => {
updateInProgress = false,
workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler,
- subscriptionHandler = initialSubscriptionHandler,
+ subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
workItemsMvc2Enabled = false,
includeWidgets = false,
error = undefined,
} = {}) => {
+ const handlers = [
+ [workItemQuery, handler],
+ [workItemTitleSubscription, subscriptionHandler],
+ [workItemDatesSubscription, datesSubscriptionHandler],
+ confidentialityMock,
+ ];
+
+ if (IS_EE) {
+ handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
+ }
+
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(
- [
- [workItemQuery, handler],
- [workItemTitleSubscription, subscriptionHandler],
- confidentialityMock,
- ],
+ handlers,
{},
{
typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
@@ -93,6 +106,7 @@ describe('WorkItemDetail component', () => {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
},
+ hasIssueWeightsFeature: true,
},
});
};
@@ -134,6 +148,10 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemState().exists()).toBe(true);
expect(findWorkItemTitle().exists()).toBe(true);
});
+
+ it('updates the document title', () => {
+ expect(document.title).toEqual('Updated title · Task · test-project-path');
+ });
});
describe('close button', () => {
@@ -295,8 +313,7 @@ describe('WorkItemDetail component', () => {
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await waitForPromises();
-
- expect(wrapper.emitted('workItemUpdated')).toBeFalsy();
+ expect(wrapper.emitted('workItemUpdated')).toBeUndefined();
await nextTick();
@@ -379,23 +396,50 @@ describe('WorkItemDetail component', () => {
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
createComponent();
await waitForPromises();
+ const updateError = 'Failed to update';
- findWorkItemTitle().vm.$emit('error', i18n.updateError);
+ findWorkItemTitle().vm.$emit('error', updateError);
await waitForPromises();
- expect(findAlert().text()).toBe(i18n.updateError);
+ expect(findAlert().text()).toBe(updateError);
});
- it('calls the subscription', () => {
- createComponent();
+ describe('subscriptions', () => {
+ it('calls the title subscription', () => {
+ createComponent();
+
+ expect(titleSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
- expect(initialSubscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
+ describe('dates subscription', () => {
+ describe('when the due date widget exists', () => {
+ it('calls the dates subscription', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(datesSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+ });
+
+ describe('when the due date widget does not exist', () => {
+ it('does not call the dates subscription', async () => {
+ const response = workItemResponseFactory({ datesWidgetPresent: false });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(datesSubscriptionHandler).not.toHaveBeenCalled();
+ });
+ });
});
});
- describe('when work_items_mvc_2 feature flag is enabled', () => {
- it('renders assignees component when assignees widget is returned from the API', async () => {
+ describe('assignees widget', () => {
+ it('renders assignees component when widget is returned from the API', async () => {
createComponent({
workItemsMvc2Enabled: true,
});
@@ -404,7 +448,7 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemAssignees().exists()).toBe(true);
});
- it('does not render assignees component when assignees widget is not returned from the API', async () => {
+ it('does not render assignees component when widget is not returned from the API', async () => {
createComponent({
workItemsMvc2Enabled: true,
handler: jest
@@ -417,13 +461,6 @@ describe('WorkItemDetail component', () => {
});
});
- it('does not render assignees component when assignees feature flag is disabled', async () => {
- createComponent();
- await waitForPromises();
-
- expect(findWorkItemAssignees().exists()).toBe(false);
- });
-
describe('labels widget', () => {
it.each`
description | includeWidgets | exists
@@ -437,30 +474,31 @@ describe('WorkItemDetail component', () => {
});
});
- describe('weight widget', () => {
+ describe('dates widget', () => {
describe.each`
- description | weightWidgetPresent | exists
- ${'when widget is returned from API'} | ${true} | ${true}
- ${'when widget is not returned from API'} | ${false} | ${false}
- `('$description', ({ weightWidgetPresent, exists }) => {
- it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => {
- const response = workItemResponseFactory({ weightWidgetPresent });
+ description | datesWidgetPresent | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ datesWidgetPresent, exists }) => {
+ it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
+ const response = workItemResponseFactory({ datesWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
- expect(findWorkItemWeight().exists()).toBe(exists);
+ expect(findWorkItemDueDate().exists()).toBe(exists);
});
});
it('shows an error message when it emits an `error` event', async () => {
createComponent({ workItemsMvc2Enabled: true });
await waitForPromises();
+ const updateError = 'Failed to update';
- findWorkItemWeight().vm.$emit('error', i18n.updateError);
+ findWorkItemDueDate().vm.$emit('error', updateError);
await waitForPromises();
- expect(findAlert().text()).toBe(i18n.updateError);
+ expect(findAlert().text()).toBe(updateError);
});
});
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
new file mode 100644
index 00000000000..1d76154a1f0
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -0,0 +1,346 @@
+import { GlFormGroup, GlDatepicker } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, updateWorkItemMutationErrorResponse } from '../mock_data';
+
+describe('WorkItemDueDate component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const updateWorkItemMutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const findStartDateButton = () =>
+ wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addStartDate });
+ const findStartDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.startDate);
+ const findStartDatePicker = () => wrapper.findComponent(GlDatepicker);
+ const findDueDateButton = () =>
+ wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addDueDate });
+ const findDueDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.dueDate);
+ const findDueDatePicker = () => wrapper.findAllComponents(GlDatepicker).at(1);
+ const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
+
+ const createComponent = ({
+ canUpdate = false,
+ dueDate = null,
+ startDate = null,
+ mutationHandler = updateWorkItemMutationHandler,
+ } = {}) => {
+ wrapper = mountExtended(WorkItemDueDate, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
+ propsData: {
+ canUpdate,
+ dueDate,
+ startDate,
+ workItemId,
+ workItemType: 'Task',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when can update', () => {
+ describe('start date', () => {
+ describe('`Add start date` button', () => {
+ describe.each`
+ description | startDate | exists
+ ${'when there is no start date'} | ${null} | ${true}
+ ${'when there is a start date'} | ${'2022-01-01'} | ${false}
+ `('$description', ({ startDate, exists }) => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, startDate });
+ });
+
+ it(exists ? 'renders' : 'does not render', () => {
+ expect(findStartDateButton().exists()).toBe(exists);
+ });
+ });
+
+ describe('when it emits `click` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, startDate: null });
+ findStartDateButton().vm.$emit('click');
+ });
+
+ it('renders start date picker', () => {
+ expect(findStartDateInput().exists()).toBe(true);
+ });
+
+ it('hides itself', () => {
+ expect(findStartDateButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('date picker', () => {
+ describe('when it emits a `clear` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ findStartDatePicker().vm.$emit('clear');
+ });
+
+ it('hides the date picker', () => {
+ expect(findStartDateInput().exists()).toBe(false);
+ });
+
+ it('shows the `Add start date` button', () => {
+ expect(findStartDateButton().exists()).toBe(true);
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: new Date('2022-01-01T00:00:00.000Z'),
+ startDate: null,
+ },
+ },
+ });
+ });
+ });
+
+ describe('when it emits a `close` event', () => {
+ describe('when the start date is earlier than the due date', () => {
+ const startDate = new Date('2022-01-01T00:00:00.000Z');
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+ findStartDatePicker().vm.$emit('input', startDate);
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: new Date('2022-12-31T00:00:00.000Z'),
+ startDate,
+ },
+ },
+ });
+ });
+ });
+
+ describe('when the start date is later than the due date', () => {
+ const startDate = new Date('2030-01-01T00:00:00.000Z');
+ let datePickerOpenSpy;
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+ datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show');
+ findStartDatePicker().vm.$emit('input', startDate);
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('does not call a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+
+ it('updates the due date picker to the same date', () => {
+ expect(findDueDatePicker().props('value')).toEqual(startDate);
+ });
+
+ it('opens the due date picker', () => {
+ expect(datePickerOpenSpy).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+
+ describe('due date', () => {
+ describe('`Add due date` button', () => {
+ describe.each`
+ description | dueDate | exists
+ ${'when there is no due date'} | ${null} | ${true}
+ ${'when there is a due date'} | ${'2022-01-01'} | ${false}
+ `('$description', ({ dueDate, exists }) => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate });
+ });
+
+ it(exists ? 'renders' : 'does not render', () => {
+ expect(findDueDateButton().exists()).toBe(exists);
+ });
+ });
+
+ describe('when it emits `click` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: null });
+ findDueDateButton().vm.$emit('click');
+ });
+
+ it('renders due date picker', () => {
+ expect(findDueDateInput().exists()).toBe(true);
+ });
+
+ it('hides itself', () => {
+ expect(findDueDateButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('date picker', () => {
+ describe('when it emits a `clear` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ findDueDatePicker().vm.$emit('clear');
+ });
+
+ it('hides the date picker', () => {
+ expect(findDueDateInput().exists()).toBe(false);
+ });
+
+ it('shows the `Add due date` button', () => {
+ expect(findDueDateButton().exists()).toBe(true);
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: null,
+ startDate: new Date('2022-01-01T00:00:00.000Z'),
+ },
+ },
+ });
+ });
+ });
+
+ describe('when it emits a `close` event', () => {
+ const dueDate = new Date('2022-12-31T00:00:00.000Z');
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ findDueDatePicker().vm.$emit('input', dueDate);
+ findDueDatePicker().vm.$emit('close');
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate,
+ startDate: new Date('2022-01-01T00:00:00.000Z'),
+ },
+ },
+ });
+ });
+ });
+ });
+ });
+
+ describe('when updating date', () => {
+ describe('when dates are changed', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z'));
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('mutation is called to update dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: new Date('2022-12-31T00:00:00.000Z'),
+ startDate: new Date('2022-01-01T00:00:00.000Z'),
+ },
+ },
+ });
+ });
+
+ it('start date input is disabled', () => {
+ expect(findStartDatePicker().props('disabled')).toBe(true);
+ });
+
+ it('due date input is disabled', () => {
+ expect(findDueDatePicker().props('disabled')).toBe(true);
+ });
+
+ it('tracks updating the dates', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_dates', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_dates',
+ property: 'type_Task',
+ });
+ });
+ });
+
+ describe('when dates are unchanged', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+
+ findStartDatePicker().vm.$emit('input', new Date('2022-12-31T00:00:00.000Z'));
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('mutation is not called to update dates', () => {
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ description | mutationHandler
+ ${'when there is a GraphQL error'} | ${jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse)}
+ ${'when there is a network error'} | ${jest.fn().mockRejectedValue(new Error())}
+ `('$description', ({ mutationHandler }) => {
+ beforeEach(() => {
+ createComponent({
+ canUpdate: true,
+ dueDate: '2022-12-31',
+ startDate: '2022-12-31',
+ mutationHandler,
+ });
+
+ findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z'));
+ findStartDatePicker().vm.$emit('close');
+ return waitForPromises();
+ });
+
+ it('emits an error', () => {
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
+ });
+ });
+ });
+ });
+
+ describe('when cannot update', () => {
+ it('start and due date inputs are disabled', async () => {
+ createComponent({ canUpdate: false, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ await nextTick();
+
+ expect(findStartDateInput().props('disabled')).toBe(true);
+ expect(findDueDateInput().props('disabled')).toBe(true);
+ });
+
+ describe('when there is no start and due date', () => {
+ it('shows None', () => {
+ createComponent({ canUpdate: false, dueDate: null, startDate: null });
+
+ expect(findGlFormGroup().text()).toContain(WorkItemDueDate.i18n.none);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js
index d5f6921c2bc..887c5f615e9 100644
--- a/spec/frontend/work_items/components/work_item_information_spec.js
+++ b/spec/frontend/work_items/components/work_item_information_spec.js
@@ -8,7 +8,6 @@ const createComponent = () => mount(WorkItemInformation);
describe('Work item information alert', () => {
let wrapper;
const tasksHelpPath = helpPagePath('user/tasks');
- const workItemsHelpPath = helpPagePath('development/work_items');
const findAlert = () => wrapper.findComponent(GlAlert);
const findHelpLink = () => wrapper.findComponent(GlLink);
@@ -33,16 +32,12 @@ describe('Work item information alert', () => {
expect(findAlert().props('variant')).toBe('tip');
});
- it('should have the correct text for primary button and link', () => {
+ it('should have the correct text for title', () => {
expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle);
- expect(findAlert().props('primaryButtonText')).toBe(
- WorkItemInformation.i18n.learnTasksButtonText,
- );
- expect(findAlert().props('primaryButtonLink')).toBe(tasksHelpPath);
});
it('should have the correct link to work item link', () => {
expect(findHelpLink().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(workItemsHelpPath);
+ expect(findHelpLink().attributes('href')).toBe(tasksHelpPath);
});
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 1734b901d1a..1d976897c15 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -9,7 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { i18n } from '~/work_items/constants';
-import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
+import { temporaryConfig, resolvers } from '~/graphql_shared/issuable_client';
import { projectLabelsResponse, mockLabels, workItemQueryResponse } from '../mock_data';
Vue.use(VueApollo);
@@ -45,13 +45,11 @@ describe('WorkItemLabels component', () => {
});
wrapper = mountExtended(WorkItemLabels, {
- provide: {
- fullPath: 'test-project-path',
- },
propsData: {
labels,
workItemId,
canUpdate,
+ fullPath: 'test-project-path',
},
attachTo: document.body,
apolloProvider,
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
new file mode 100644
index 00000000000..1d5472a0473
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -0,0 +1,122 @@
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
+
+import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data';
+
+describe('WorkItemLinkChild', () => {
+ const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
+ let wrapper;
+
+ const createComponent = ({
+ projectPath = 'gitlab-org/gitlab-test',
+ canUpdate = true,
+ issuableGid = WORK_ITEM_ID,
+ childItem = workItemTask,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinkChild, {
+ propsData: {
+ projectPath,
+ canUpdate,
+ issuableGid,
+ childItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
+ ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
+ ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
+ `(
+ 'renders item status icon and tooltip when item status is `$status`',
+ ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
+ createComponent({ childItem });
+
+ const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
+ const statusTooltip = wrapper.findComponent(RichTimestampTooltip);
+
+ expect(statusIcon.props('name')).toBe(statusIconName);
+ expect(statusIcon.classes()).toContain(statusIconColorClass);
+ expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp);
+ expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents);
+ },
+ );
+
+ it('renders confidential icon when item is confidential', () => {
+ createComponent({ childItem: confidentialWorkItemTask });
+
+ const confidentialIcon = wrapper.findByTestId('confidential-icon');
+
+ expect(confidentialIcon.props('name')).toBe('eye-slash');
+ expect(confidentialIcon.attributes('title')).toBe('Confidential');
+ });
+
+ describe('item title', () => {
+ let titleEl;
+
+ beforeEach(() => {
+ createComponent();
+
+ titleEl = wrapper.findComponent(GlButton);
+ });
+
+ it('renders item title', () => {
+ expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
+ expect(titleEl.text()).toBe(workItemTask.title);
+ });
+
+ it.each`
+ action | event | emittedEvent
+ ${'clicking'} | ${'click'} | ${'click'}
+ ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
+ ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
+ `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
+ const eventObj = {
+ preventDefault: jest.fn(),
+ };
+ titleEl.vm.$emit(event, eventObj);
+
+ expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]);
+ });
+ });
+
+ describe('item menu', () => {
+ let itemMenuEl;
+
+ beforeEach(() => {
+ createComponent();
+
+ itemMenuEl = wrapper.findComponent(WorkItemLinksMenu);
+ });
+
+ it('renders work-item-links-menu', () => {
+ expect(itemMenuEl.exists()).toBe(true);
+
+ expect(itemMenuEl.attributes()).toMatchObject({
+ 'work-item-id': workItemTask.id,
+ 'parent-work-item-id': WORK_ITEM_ID,
+ });
+ });
+
+ it('does not render work-item-links-menu when canUpdate is false', () => {
+ createComponent({ canUpdate: false });
+
+ expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false);
+ });
+
+ it('removeChild event on menu triggers `click-remove-child` event', () => {
+ itemMenuEl.vm.$emit('removeChild');
+
+ expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 00f508f1548..876aedff08b 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,12 +1,13 @@
import Vue, { nextTick } from 'vue';
-import { GlButton, GlIcon, GlAlert } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import SidebarEventHub from '~/sidebar/event_hub';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
@@ -20,6 +21,20 @@ import {
Vue.use(VueApollo);
+const issueConfidentialityResponse = (confidential = false) => ({
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ },
+ },
+ },
+});
+
describe('WorkItemLinks', () => {
let wrapper;
let mockApollo;
@@ -36,18 +51,18 @@ describe('WorkItemLinks', () => {
const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const findChildren = () => wrapper.findAll('[data-testid="links-child"]');
-
const createComponent = async ({
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
+ confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()),
} = {}) => {
mockApollo = createMockApollo(
[
[getWorkItemLinksQuery, fetchHandler],
[changeWorkItemParentMutation, mutationHandler],
[workItemQuery, childWorkItemQueryHandler],
+ [issueConfidentialQuery, confidentialQueryHandler],
],
{},
{ addTypename: true },
@@ -61,6 +76,7 @@ describe('WorkItemLinks', () => {
},
provide: {
projectPath: 'project/path',
+ iid: '1',
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -77,8 +93,9 @@ describe('WorkItemLinks', () => {
const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
+ const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
+ const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
- const findFirstLinksMenu = () => wrapper.findByTestId('links-menu');
const findChildrenCount = () => wrapper.findByTestId('children-count');
beforeEach(async () => {
@@ -132,8 +149,7 @@ describe('WorkItemLinks', () => {
it('renders all hierarchy widget children', () => {
expect(findLinksBody().exists()).toBe(true);
- expect(findChildren()).toHaveLength(4);
- expect(findFirstLinksMenu().exists()).toBe(true);
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
it('shows alert when list loading fails', async () => {
@@ -148,40 +164,12 @@ describe('WorkItemLinks', () => {
expect(findAlert().text()).toBe(errorMessage);
});
- it('renders widget child icon and tooltip', () => {
- expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m');
- expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close');
- });
-
- it('renders confidentiality icon when child item is confidential', () => {
- const children = wrapper.findAll('[data-testid="links-child"]');
- const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]');
-
- expect(confidentialIcon.exists()).toBe(true);
- expect(confidentialIcon.props('name')).toBe('eye-slash');
- });
-
it('displays number if children', () => {
expect(findChildrenCount().exists()).toBe(true);
expect(findChildrenCount().text()).toContain('4');
});
- it('refetches child items when `confidentialityUpdated` event is emitted on SidebarEventhub', async () => {
- const fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse);
- await createComponent({
- fetchHandler,
- });
- await waitForPromises();
-
- SidebarEventHub.$emit('confidentialityUpdated');
- await nextTick();
-
- // First call is done on component mount.
- // Second call is done on confidentialityUpdated event.
- expect(fetchHandler).toHaveBeenCalledTimes(2);
- });
-
describe('when no permission to update', () => {
beforeEach(async () => {
await createComponent({
@@ -194,17 +182,21 @@ describe('WorkItemLinks', () => {
});
it('does not display link menu on children', () => {
- expect(findFirstLinksMenu().exists()).toBe(false);
+ expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false);
});
});
describe('remove child', () => {
+ let firstChild;
+
beforeEach(async () => {
await createComponent({ mutationHandler: mutationChangeParentHandler });
+
+ firstChild = findFirstWorkItemLinkChild();
});
it('calls correct mutation with correct variables', async () => {
- findFirstLinksMenu().vm.$emit('removeChild');
+ firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises();
@@ -219,7 +211,7 @@ describe('WorkItemLinks', () => {
});
it('shows toast when mutation succeeds', async () => {
- findFirstLinksMenu().vm.$emit('removeChild');
+ firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises();
@@ -229,28 +221,30 @@ describe('WorkItemLinks', () => {
});
it('renders correct number of children after removal', async () => {
- expect(findChildren()).toHaveLength(4);
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
- findFirstLinksMenu().vm.$emit('removeChild');
+ firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises();
- expect(findChildren()).toHaveLength(3);
+ expect(findWorkItemLinkChildItems()).toHaveLength(3);
});
});
describe('prefetching child items', () => {
+ let firstChild;
+
beforeEach(async () => {
await createComponent();
- });
- const findChildLink = () => findChildren().at(0).findComponent(GlButton);
+ firstChild = findFirstWorkItemLinkChild();
+ });
it('does not fetch the child work item before hovering work item links', () => {
expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
});
it('fetches the child work item if link is hovered for 250+ ms', async () => {
- findChildLink().vm.$emit('mouseover');
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises();
@@ -260,12 +254,24 @@ describe('WorkItemLinks', () => {
});
it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
- findChildLink().vm.$emit('mouseover');
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
jest.advanceTimersByTime(200);
- findChildLink().vm.$emit('mouseout');
+ firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
await waitForPromises();
expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
});
});
+
+ describe('when parent item is confidential', () => {
+ it('passes correct confidentiality status to form', async () => {
+ await createComponent({
+ confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)),
+ });
+ findToggleAddFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findAddLinksForm().props('parentConfidential')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
index 6b23a6e4795..b24d940d56a 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import ItemState from '~/work_items/components/item_state.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import {
- i18n,
STATE_OPEN,
STATE_CLOSED,
STATE_EVENT_CLOSE,
@@ -104,7 +103,9 @@ describe('WorkItemState component', () => {
findItemState().vm.$emit('changed', STATE_CLOSED);
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
});
it('tracks editing the state', async () => {
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index c0d966abab8..a549aad5cd8 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -6,7 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
-import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
@@ -116,7 +116,9 @@ describe('WorkItemTitle component', () => {
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
});
it('tracks editing the title', async () => {
diff --git a/spec/frontend/work_items/components/work_item_type_icon_spec.js b/spec/frontend/work_items/components/work_item_type_icon_spec.js
index 85466578e18..95ddfc3980e 100644
--- a/spec/frontend/work_items/components/work_item_type_icon_spec.js
+++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js
@@ -1,11 +1,17 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
let wrapper;
function createComponent(propsData) {
- wrapper = shallowMount(WorkItemTypeIcon, { propsData });
+ wrapper = shallowMount(WorkItemTypeIcon, {
+ propsData,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
}
describe('Work Item type component', () => {
@@ -16,22 +22,23 @@ describe('Work Item type component', () => {
});
describe.each`
- workItemType | workItemIconName | iconName | text
- ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'}
- ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''}
- ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'}
- ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''}
- ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'}
- ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'}
- ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'}
- ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''}
+ workItemType | workItemIconName | iconName | text | showTooltipOnHover
+ ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false}
+ ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} | ${true}
+ ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true}
+ ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} | ${true}
+ ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true}
+ ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false}
+ ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true}
+ ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} | ${true}
`(
'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
- ({ workItemType, workItemIconName, iconName, text }) => {
+ ({ workItemType, workItemIconName, iconName, text, showTooltipOnHover }) => {
beforeEach(() => {
createComponent({
workItemType,
workItemIconName,
+ showTooltipOnHover,
});
});
@@ -42,6 +49,16 @@ describe('Work Item type component', () => {
it(`renders correct text`, () => {
expect(wrapper.text()).toBe(text);
});
+
+ it('renders the icon in gray color', () => {
+ expect(findIcon().classes()).toContain('gl-text-gray-500');
+ });
+
+ it('shows tooltip on hover when props passed', () => {
+ const tooltip = getBinding(findIcon().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(showTooltipOnHover);
+ });
},
);
});
diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js
deleted file mode 100644
index 94bdb336deb..00000000000
--- a/spec/frontend/work_items/components/work_item_weight_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { GlForm, GlFormInput } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { mockTracking } from 'helpers/tracking_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { __ } from '~/locale';
-import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
-import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
-import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
-
-describe('WorkItemWeight component', () => {
- Vue.use(VueApollo);
-
- let wrapper;
-
- const workItemId = 'gid://gitlab/WorkItem/1';
- const workItemType = 'Task';
-
- const findForm = () => wrapper.findComponent(GlForm);
- const findInput = () => wrapper.findComponent(GlFormInput);
-
- const createComponent = ({
- canUpdate = false,
- hasIssueWeightsFeature = true,
- isEditing = false,
- weight,
- mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse),
- } = {}) => {
- wrapper = mountExtended(WorkItemWeight, {
- apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
- propsData: {
- canUpdate,
- weight,
- workItemId,
- workItemType,
- },
- provide: {
- hasIssueWeightsFeature,
- },
- });
-
- if (isEditing) {
- findInput().vm.$emit('focus');
- }
- };
-
- describe('`issue_weights` licensed feature', () => {
- describe.each`
- description | hasIssueWeightsFeature | exists
- ${'when available'} | ${true} | ${true}
- ${'when not available'} | ${false} | ${false}
- `('$description', ({ hasIssueWeightsFeature, exists }) => {
- it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => {
- createComponent({ hasIssueWeightsFeature });
-
- expect(findForm().exists()).toBe(exists);
- });
- });
- });
-
- describe('weight input', () => {
- it('has "Weight" label', () => {
- createComponent();
-
- expect(wrapper.findByLabelText(__('Weight')).exists()).toBe(true);
- });
-
- describe('placeholder attribute', () => {
- describe.each`
- description | isEditing | canUpdate | value
- ${'when not editing and cannot update'} | ${false} | ${false} | ${__('None')}
- ${'when editing and cannot update'} | ${true} | ${false} | ${__('None')}
- ${'when not editing and can update'} | ${false} | ${true} | ${__('None')}
- ${'when editing and can update'} | ${true} | ${true} | ${__('Enter a number')}
- `('$description', ({ isEditing, canUpdate, value }) => {
- it(`has a value of "${value}"`, async () => {
- createComponent({ canUpdate, isEditing });
- await nextTick();
-
- expect(findInput().attributes('placeholder')).toBe(value);
- });
- });
- });
-
- describe('readonly attribute', () => {
- describe.each`
- description | canUpdate | value
- ${'when cannot update'} | ${false} | ${'readonly'}
- ${'when can update'} | ${true} | ${undefined}
- `('$description', ({ canUpdate, value }) => {
- it(`renders readonly=${value}`, () => {
- createComponent({ canUpdate });
-
- expect(findInput().attributes('readonly')).toBe(value);
- });
- });
- });
-
- describe('type attribute', () => {
- describe.each`
- description | isEditing | canUpdate | type
- ${'when not editing and cannot update'} | ${false} | ${false} | ${'text'}
- ${'when editing and cannot update'} | ${true} | ${false} | ${'text'}
- ${'when not editing and can update'} | ${false} | ${true} | ${'text'}
- ${'when editing and can update'} | ${true} | ${true} | ${'number'}
- `('$description', ({ isEditing, canUpdate, type }) => {
- it(`has a value of "${type}"`, async () => {
- createComponent({ canUpdate, isEditing });
- await nextTick();
-
- expect(findInput().attributes('type')).toBe(type);
- });
- });
- });
-
- describe('value attribute', () => {
- describe.each`
- weight | value
- ${1} | ${'1'}
- ${0} | ${'0'}
- ${null} | ${''}
- ${undefined} | ${''}
- `('when `weight` prop is "$weight"', ({ weight, value }) => {
- it(`value is "${value}"`, () => {
- createComponent({ weight });
-
- expect(findInput().element.value).toBe(value);
- });
- });
- });
-
- describe('when blurred', () => {
- it('calls a mutation to update the weight when the input value is different', () => {
- const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
- createComponent({
- isEditing: true,
- weight: 0,
- mutationHandler: mutationSpy,
- canUpdate: true,
- });
-
- findInput().vm.$emit('blur', { target: { value: 1 } });
-
- expect(mutationSpy).toHaveBeenCalledWith({
- input: {
- id: workItemId,
- weightWidget: {
- weight: 1,
- },
- },
- });
- });
-
- it('does not call a mutation to update the weight when the input value is the same', () => {
- const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
- createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true });
-
- findInput().trigger('blur');
-
- expect(mutationSpy).not.toHaveBeenCalledWith();
- });
-
- it('emits an error when there is a GraphQL error', async () => {
- const response = {
- data: {
- workItemUpdate: {
- errors: ['Error!'],
- workItem: {},
- },
- },
- };
- createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockResolvedValue(response),
- canUpdate: true,
- });
-
- findInput().trigger('blur');
- await waitForPromises();
-
- expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
- });
-
- it('emits an error when there is a network error', async () => {
- createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockRejectedValue(new Error()),
- canUpdate: true,
- });
-
- findInput().trigger('blur');
- await waitForPromises();
-
- expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
- });
-
- it('tracks updating the weight', () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- createComponent({ canUpdate: true });
-
- findInput().trigger('blur');
-
- expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_weight',
- property: 'type_Task',
- });
- });
- });
- });
-});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d24ac2a9f93..e1bc8d2f6b7 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -28,6 +28,11 @@ export const workItemQueryResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -93,6 +98,11 @@ export const updateWorkItemMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -128,6 +138,16 @@ export const updateWorkItemMutationResponse = {
},
};
+export const updateWorkItemMutationErrorResponse = {
+ data: {
+ workItemUpdate: {
+ __typename: 'WorkItemUpdatePayload',
+ errors: ['Error!'],
+ workItem: {},
+ },
+ },
+};
+
export const mockParent = {
parent: {
id: 'gid://gitlab/Issue/1',
@@ -142,6 +162,7 @@ export const workItemResponseFactory = ({
canDelete = false,
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
+ datesWidgetPresent = true,
weightWidgetPresent = true,
confidential = false,
canInviteMembers = false,
@@ -157,6 +178,11 @@ export const workItemResponseFactory = ({
confidential,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -186,6 +212,14 @@ export const workItemResponseFactory = ({
},
}
: { type: 'MOCK TYPE' },
+ datesWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ type: 'START_AND_DUE_DATE',
+ dueDate: '2022-12-31',
+ startDate: '2022-01-01',
+ }
+ : { type: 'MOCK TYPE' },
weightWidgetPresent
? {
__typename: 'WorkItemWidgetWeight',
@@ -212,17 +246,6 @@ export const workItemResponseFactory = ({
},
});
-export const updateWorkItemWidgetsResponse = {
- data: {
- workItemUpdateWidgets: {
- workItem: {
- id: 1234,
- },
- errors: [],
- },
- },
-};
-
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -251,6 +274,11 @@ export const createWorkItemMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -282,6 +310,11 @@ export const createWorkItemFromTaskMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -310,6 +343,11 @@ export const createWorkItemFromTaskMutationResponse = {
closedAt: null,
description: '',
confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -368,6 +406,21 @@ export const deleteWorkItemFromTaskMutationErrorResponse = {
},
};
+export const workItemDatesSubscriptionResponse = {
+ data: {
+ issuableDatesUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ dueDate: '2022-12-31',
+ startDate: '2022-01-01',
+ },
+ ],
+ },
+ },
+};
+
export const workItemTitleSubscriptionResponse = {
data: {
issuableTitleUpdated: {
@@ -377,6 +430,20 @@ export const workItemTitleSubscriptionResponse = {
},
};
+export const workItemWeightSubscriptionResponse = {
+ data: {
+ issuableWeightUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetWeight',
+ weight: 1,
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
@@ -388,6 +455,11 @@ export const workItemHierarchyEmptyResponse = {
title: 'New title',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
@@ -426,6 +498,11 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
confidential: false,
widgets: [
{
@@ -461,6 +538,48 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
},
};
+export const workItemTask = {
+ id: 'gid://gitlab/WorkItem/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'bar',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+};
+
+export const confidentialWorkItemTask = {
+ id: 'gid://gitlab/WorkItem/2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'xyz',
+ state: 'OPEN',
+ confidential: true,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+};
+
+export const closedWorkItemTask = {
+ id: 'gid://gitlab/WorkItem/3',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'abc',
+ state: 'CLOSED',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: '2022-08-12T13:07:52Z',
+ __typename: 'WorkItem',
+};
+
export const workItemHierarchyResponse = {
data: {
workItem: {
@@ -475,6 +594,11 @@ export const workItemHierarchyResponse = {
updateWorkItem: true,
},
confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
widgets: [
{
type: 'DESCRIPTION',
@@ -485,45 +609,9 @@ export const workItemHierarchyResponse = {
parent: null,
children: {
nodes: [
- {
- id: 'gid://gitlab/WorkItem/2',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
- __typename: 'WorkItemType',
- },
- title: 'xyz',
- state: 'OPEN',
- confidential: true,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: null,
- __typename: 'WorkItem',
- },
- {
- id: 'gid://gitlab/WorkItem/3',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
- __typename: 'WorkItemType',
- },
- title: 'abc',
- state: 'CLOSED',
- confidential: false,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: '2022-08-12T13:07:52Z',
- __typename: 'WorkItem',
- },
- {
- id: 'gid://gitlab/WorkItem/4',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
- __typename: 'WorkItemType',
- },
- title: 'bar',
- state: 'OPEN',
- confidential: false,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: null,
- __typename: 'WorkItem',
- },
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ workItemTask,
{
id: 'gid://gitlab/WorkItem/5',
workItemType: {
@@ -570,6 +658,11 @@ export const changeWorkItemParentMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
widgets: [
{
__typename: 'WorkItemWidgetHierarchy',
@@ -649,6 +742,71 @@ export const projectMembersResponseWithCurrentUser = {
},
},
],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ startCursor: null,
+ },
+ },
+ },
+ },
+};
+
+export const projectMembersResponseWithCurrentUserWithNextPage = {
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ users: {
+ nodes: [
+ {
+ id: 'user-2',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/5',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+ },
+ },
+ {
+ id: 'user-1',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ },
+ ],
+ pageInfo: {
+ hasNextPage: true,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ },
+ },
+ },
+};
+
+export const projectMembersResponseWithNoMatchingUsers = {
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ users: {
+ nodes: [],
+ pageInfo: {
+ endCursor: null,
+ hasNextPage: false,
+ startCursor: null,
+ },
},
},
},
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index fed8be3783a..15dac25b7d9 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -193,6 +193,8 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
- expect(findAlert().text()).toBe(CreateWorkItem.createErrorText);
+ expect(findAlert().text()).toBe(
+ 'Something went wrong when creating work item. Please try again.',
+ );
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 99dcd886f7b..ab370e2ca8b 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -1,5 +1,18 @@
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import {
+ workItemDatesSubscriptionResponse,
+ workItemResponseFactory,
+ workItemTitleSubscriptionResponse,
+ workItemWeightSubscriptionResponse,
+} from 'jest/work_items/mock_data';
import App from '~/work_items/components/app.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
+import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
@@ -7,26 +20,36 @@ import { createRouter } from '~/work_items/router';
describe('Work items router', () => {
let wrapper;
+ Vue.use(VueApollo);
+
+ const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
+ const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
+ const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
+
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
if (routeArg !== undefined) {
await router.push(routeArg);
}
+ const handlers = [
+ [workItemQuery, workItemQueryHandler],
+ [workItemDatesSubscription, datesSubscriptionHandler],
+ [workItemTitleSubscription, titleSubscriptionHandler],
+ ];
+
+ if (IS_EE) {
+ handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
+ }
+
wrapper = mount(App, {
+ apolloProvider: createMockApollo(handlers),
router,
provide: {
fullPath: 'full-path',
issuesListPath: 'full-path/-/issues',
},
- mocks: {
- $apollo: {
- queries: {
- workItem: {},
- workItemTypes: {},
- },
- },
- },
});
};
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
index 67420e7fc2a..dca016dc317 100644
--- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -74,7 +74,7 @@ describe('WorkItemsHierarchy Hierarchy', () => {
});
it('renders license badges for all work items', () => {
- expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
+ expect(wrapper.findAllComponents(GlBadge)).toHaveLength(items.length);
});
it('does not render svg icon for linking', () => {
diff --git a/spec/frontend_integration/content_editor/content_editor_integration_spec.js b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
index 12cd6dcad83..c0c6b5e5dc8 100644
--- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js
+++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
@@ -1,6 +1,7 @@
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { ContentEditor } from '~/content_editor';
+import waitForPromises from 'helpers/wait_for_promises';
/**
* This spec exercises some workflows in the Content Editor without mocking
@@ -10,32 +11,38 @@ import { ContentEditor } from '~/content_editor';
describe('content_editor', () => {
let wrapper;
let renderMarkdown;
- let contentEditorService;
- const buildWrapper = () => {
- renderMarkdown = jest.fn();
+ const buildWrapper = ({ markdown = '' } = {}) => {
wrapper = mountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath: '/',
- },
- listeners: {
- initialized(contentEditor) {
- contentEditorService = contentEditor;
- },
+ markdown,
},
});
};
+ const waitUntilContentIsLoaded = async () => {
+ await waitForPromises();
+ await nextTick();
+ };
+
+ const mockRenderMarkdownResponse = (response) => {
+ renderMarkdown.mockImplementation((markdown) => (markdown ? response : null));
+ };
+
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ });
+
describe('when loading initial content', () => {
describe('when the initial content is empty', () => {
it('still hides the loading indicator', async () => {
- buildWrapper();
+ mockRenderMarkdownResponse('');
- renderMarkdown.mockResolvedValue('');
+ buildWrapper();
- await contentEditorService.setSerializedContent('');
- await nextTick();
+ await waitUntilContentIsLoaded();
expect(wrapper.findByTestId('content-editor-loading-indicator').exists()).toBe(false);
});
@@ -44,14 +51,15 @@ describe('content_editor', () => {
describe('when the initial content is not empty', () => {
const initialContent = '<p><strong>bold text</strong></p>';
beforeEach(async () => {
- buildWrapper();
+ mockRenderMarkdownResponse(initialContent);
- renderMarkdown.mockResolvedValue(initialContent);
+ buildWrapper({
+ markdown: '**bold text**',
+ });
- await contentEditorService.setSerializedContent('**bold text**');
- await nextTick();
+ await waitUntilContentIsLoaded();
});
- it('hides the loading indicator', async () => {
+ it('hides the loading indicator', () => {
expect(wrapper.findByTestId('content-editor-loading-indicator').exists()).toBe(false);
});
@@ -70,27 +78,29 @@ describe('content_editor', () => {
});
it('processes and renders footnote ids alongside the footnote definition', async () => {
- buildWrapper();
-
- await contentEditorService.setSerializedContent(`
+ buildWrapper({
+ markdown: `
This reference tag is a mix of letters and numbers [^footnote].
[^footnote]: This is another footnote.
- `);
- await nextTick();
+ `,
+ });
+
+ await waitUntilContentIsLoaded();
expect(wrapper.text()).toContain('footnote: This is another footnote');
});
it('processes and displays reference definitions', async () => {
- buildWrapper();
-
- await contentEditorService.setSerializedContent(`
+ buildWrapper({
+ markdown: `
[GitLab][gitlab]
[gitlab]: https://gitlab.com
- `);
- await nextTick();
+ `,
+ });
+
+ await waitUntilContentIsLoaded();
expect(wrapper.find('pre').text()).toContain('[gitlab]: https://gitlab.com');
});
@@ -99,9 +109,7 @@ This reference tag is a mix of letters and numbers [^footnote].
it('renders table of contents', async () => {
jest.useFakeTimers();
- buildWrapper();
-
- renderMarkdown.mockResolvedValue(`
+ renderMarkdown.mockResolvedValueOnce(`
<ul class="section-nav">
</ul>
<h1 dir="auto" data-sourcepos="3:1-3:11">
@@ -112,18 +120,53 @@ This reference tag is a mix of letters and numbers [^footnote].
</h2>
`);
- await contentEditorService.setSerializedContent(`
+ buildWrapper({
+ markdown: `
[TOC]
# Heading 1
## Heading 2
- `);
+ `,
+ });
- await nextTick();
- jest.runAllTimers();
+ await waitUntilContentIsLoaded();
expect(wrapper.findByTestId('table-of-contents').text()).toContain('Heading 1');
expect(wrapper.findByTestId('table-of-contents').text()).toContain('Heading 2');
});
+
+ describe('when pasting content', () => {
+ const buildClipboardData = (data = {}) => ({
+ clipboardData: {
+ getData(mimeType) {
+ return data[mimeType];
+ },
+ types: Object.keys(data),
+ },
+ });
+
+ describe('when the clipboard does not contain text/html data', () => {
+ it('processes the clipboard content as markdown', async () => {
+ const processedMarkdown = '<strong>bold text</strong>';
+
+ buildWrapper();
+
+ await waitUntilContentIsLoaded();
+
+ mockRenderMarkdownResponse(processedMarkdown);
+
+ wrapper.find('[contenteditable]').trigger(
+ 'paste',
+ buildClipboardData({
+ 'text/plain': '**bold text**',
+ }),
+ );
+
+ await waitUntilContentIsLoaded();
+
+ expect(wrapper.find('[contenteditable]').html()).toContain(processedMarkdown);
+ });
+ });
+ });
});
diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js
index 925db12f36e..40e6a725358 100644
--- a/spec/frontend_integration/ide/helpers/start.js
+++ b/spec/frontend_integration/ide/helpers/start.js
@@ -1,7 +1,7 @@
import { editor as monacoEditor } from 'monaco-editor';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import { initIde } from '~/ide';
+import { initLegacyWebIDE } from '~/ide';
import extendStore from '~/ide/stores/extend';
import { getProject, getEmptyProject } from 'jest/../frontend_integration/test_helpers/fixtures';
import { IDE_DATASET } from './mock_data';
@@ -16,7 +16,7 @@ export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) =
const el = document.createElement('div');
Object.assign(el.dataset, IDE_DATASET, { project: JSON.stringify(project) });
container.appendChild(el);
- const vm = initIde(el, { extendStore });
+ const vm = initLegacyWebIDE(el, { extendStore });
// We need to dispose of editor Singleton things or tests will bump into eachother
vm.$on('destroy', () => monacoEditor.getModels().forEach((model) => model.dispose()));
diff --git a/spec/graphql/features/feature_flag_spec.rb b/spec/graphql/features/feature_flag_spec.rb
deleted file mode 100644
index b06718eb16a..00000000000
--- a/spec/graphql/features/feature_flag_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Graphql Field feature flags' do
- include GraphqlHelpers
- include Graphql::ResolverFactories
-
- let_it_be(:user) { create(:user) }
-
- let(:feature_flag) { 'test_feature' }
- let(:test_object) { double(name: 'My name') }
- let(:query_string) { '{ item { name } }' }
- let(:result) { execute_query(query_type)['data'] }
-
- before do
- skip_feature_flags_yaml_validation
- end
-
- subject { result }
-
- describe 'Feature flagged field' do
- let(:type) { type_factory }
-
- let(:query_type) do
- query_factory do |query|
- query.field :item, type, null: true, _deprecated_feature_flag: feature_flag, resolver: new_resolver(test_object)
- end
- end
-
- it 'checks YAML definition for default_enabled' do
- # Exception is indicative of a check for YAML definition
- expect { subject }.to raise_error(Feature::InvalidFeatureFlagError, /The feature flag YAML definition for '#{feature_flag}' does not exist/)
- end
-
- context 'skipping YAML check' do
- before do
- skip_default_enabled_yaml_check
- end
-
- it 'returns the value when feature is enabled' do
- expect(subject['item']).to eq('name' => test_object.name)
- end
-
- it 'returns nil when the feature is disabled' do
- stub_feature_flags(feature_flag => false)
-
- expect(subject).to be_nil
- end
- end
- end
-end
diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
index 10aed8a1f00..8e9a567f614 100644
--- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
let(:move_params) { {} }
it 'generates an error' do
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'At least one of the arguments fromListId, toListId, afterId or beforeId is required') do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'At least one of the arguments fromListId, toListId, positionInList, moveAfterId, or moveBeforeId is required') do
subject
end
end
@@ -71,6 +71,50 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
end
end
+ context 'when positionInList is given' do
+ let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } }
+
+ context 'when fromListId and toListId are missing' do
+ let(:move_params) { { position_in_list: 0 } }
+
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'Both fromListId and toListId are required when positionInList is given') do
+ subject
+ end
+ end
+ end
+
+ context 'when move_before_id is also given' do
+ let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0, move_before_id: 1 } }
+
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'positionInList is mutually exclusive with any of moveBeforeId or moveAfterId') do
+ subject
+ end
+ end
+ end
+
+ context 'when move_after_id is also given' do
+ let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0, move_after_id: 1 } }
+
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'positionInList is mutually exclusive with any of moveBeforeId or moveAfterId') do
+ subject
+ end
+ end
+ end
+
+ context 'when position_in_list is invalid' do
+ let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: -5 } }
+
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, "positionInList must be >= 0 or #{Boards::Issues::MoveService::LIST_END_POSITION}") do
+ subject
+ end
+ end
+ end
+ end
+
context 'when user have access to resources' do
it 'moves and repositions issue' do
subject
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index b8efd4213fa..39fe2a53a68 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -6,10 +6,13 @@ RSpec.describe Mutations::Ci::Runner::Update do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:runner) { create(:ci_runner, active: true, locked: false, run_untagged: true) }
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:runner) do
+ create(:ci_runner, :project, projects: [project1], active: true, locked: false, run_untagged: true)
+ end
let(:current_ctx) { { current_user: user } }
- let(:mutated_runner) { subject[:runner] }
+ let(:mutated_runner) { response[:runner] }
let(:mutation_params) do
{
@@ -21,14 +24,14 @@ RSpec.describe Mutations::Ci::Runner::Update do
specify { expect(described_class).to require_graphql_authorizations(:update_runner) }
describe '#resolve' do
- subject do
+ subject(:response) do
sync(resolve(described_class, args: mutation_params, ctx: current_ctx))
end
context 'when the user cannot admin the runner' do
it 'generates an error' do
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
- subject
+ response
end
end
end
@@ -37,7 +40,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
let(:mutation_params) { {} }
it 'raises an error' do
- expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id")
+ expect { response }.to raise_error(ArgumentError, "Arguments must be provided: id")
end
end
@@ -45,41 +48,150 @@ RSpec.describe Mutations::Ci::Runner::Update do
let(:admin_user) { create(:user, :admin) }
let(:current_ctx) { { current_user: admin_user } }
- let(:mutation_params) do
- {
- id: runner.to_global_id,
- description: 'updated description',
- maintenance_note: 'updated maintenance note',
- maximum_timeout: 900,
- access_level: 'ref_protected',
- active: false,
- locked: true,
- run_untagged: false,
- tag_list: %w(tag1 tag2)
- }
- end
-
context 'with valid arguments' do
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ description: 'updated description',
+ maintenance_note: 'updated maintenance note',
+ maximum_timeout: 900,
+ access_level: 'ref_protected',
+ active: false,
+ locked: true,
+ run_untagged: false,
+ tag_list: %w(tag1 tag2)
+ }
+ end
+
it 'updates runner with correct values' do
expected_attributes = mutation_params.except(:id, :tag_list)
- subject
+ response
- expect(subject[:errors]).to be_empty
- expect(subject[:runner]).to be_an_instance_of(Ci::Runner)
- expect(subject[:runner]).to have_attributes(expected_attributes)
- expect(subject[:runner].tag_list).to contain_exactly(*mutation_params[:tag_list])
+ expect(response[:errors]).to be_empty
+ expect(response[:runner]).to be_an_instance_of(Ci::Runner)
+ expect(response[:runner]).to have_attributes(expected_attributes)
+ expect(response[:runner].tag_list).to contain_exactly(*mutation_params[:tag_list])
expect(runner.reload).to have_attributes(expected_attributes)
expect(runner.tag_list).to contain_exactly(*mutation_params[:tag_list])
end
end
+ context 'with associatedProjects argument' do
+ let_it_be(:project2) { create(:project) }
+
+ context 'with id set to project runner' do
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ description: 'updated description',
+ associated_projects: [project2.to_global_id.to_s]
+ }
+ end
+
+ it 'updates runner attributes and project relationships', :aggregate_failures do
+ expect_next_instance_of(
+ ::Ci::Runners::SetRunnerAssociatedProjectsService,
+ {
+ runner: runner,
+ current_user: admin_user,
+ project_ids: [project2.id]
+ }
+ ) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ expected_attributes = mutation_params.except(:id, :associated_projects)
+
+ response
+
+ expect(response[:errors]).to be_empty
+ expect(response[:runner]).to be_an_instance_of(Ci::Runner)
+ expect(response[:runner]).to have_attributes(expected_attributes)
+ expect(runner.reload).to have_attributes(expected_attributes)
+ expect(runner.projects).to match_array([project1, project2])
+ end
+
+ context 'with user not allowed to assign runner' do
+ before do
+ allow(admin_user).to receive(:can?).with(:assign_runner, runner).and_return(false)
+ end
+
+ it 'does not update runner', :aggregate_failures do
+ expect_next_instance_of(
+ ::Ci::Runners::SetRunnerAssociatedProjectsService,
+ {
+ runner: runner,
+ current_user: admin_user,
+ project_ids: [project2.id]
+ }
+ ) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ expected_attributes = mutation_params.except(:id, :associated_projects)
+
+ response
+
+ expect(response[:errors]).to match_array(['user not allowed to assign runner'])
+ expect(response[:runner]).to be_an_instance_of(Ci::Runner)
+ expect(response[:runner]).not_to have_attributes(expected_attributes)
+ expect(runner.reload).not_to have_attributes(expected_attributes)
+ expect(runner.projects).to match_array([project1])
+ end
+ end
+ end
+
+ context 'with id set to instance runner' do
+ let(:instance_runner) { create(:ci_runner, :instance) }
+ let(:mutation_params) do
+ {
+ id: instance_runner.to_global_id,
+ description: 'updated description',
+ associated_projects: [project2.to_global_id.to_s]
+ }
+ end
+
+ it 'raises error', :aggregate_failures do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ response
+ end
+ end
+ end
+ end
+
+ context 'with non-existing project ID in associatedProjects argument' do
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ associated_projects: ['gid://gitlab/Project/-1']
+ }
+ end
+
+ it 'does not change associated projects' do
+ expected_attributes = mutation_params.except(:id, :associated_projects)
+
+ response
+
+ expect(response[:errors]).to be_empty
+ expect(response[:runner]).to be_an_instance_of(Ci::Runner)
+ expect(response[:runner]).to have_attributes(expected_attributes)
+ expect(runner.reload).to have_attributes(expected_attributes)
+ expect(runner.projects).to match_array([project1])
+ end
+ end
+
context 'with out-of-range maximum_timeout and missing tag_list' do
- it 'returns a descriptive error' do
- mutation_params[:maximum_timeout] = 100
- mutation_params.delete(:tag_list)
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ maximum_timeout: 100,
+ run_untagged: false
+ }
+ end
- expect(subject[:errors]).to contain_exactly(
+ it 'returns a descriptive error' do
+ expect(response[:errors]).to contain_exactly(
'Maximum timeout needs to be at least 10 minutes',
'Tags list can not be empty when runner is not allowed to pick untagged jobs'
)
@@ -90,7 +202,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
it 'returns a descriptive error' do
mutation_params[:maintenance_note] = '1' * 1025
- expect(subject[:errors]).to contain_exactly(
+ expect(response[:errors]).to contain_exactly(
'Maintenance note is too long (maximum is 1024 characters)'
)
end
diff --git a/spec/graphql/mutations/commits/create_spec.rb b/spec/graphql/mutations/commits/create_spec.rb
index 9fc9c731b96..fd0c2c46b2e 100644
--- a/spec/graphql/mutations/commits/create_spec.rb
+++ b/spec/graphql/mutations/commits/create_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Mutations::Commits::Create do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:group) { create(:group, :public) }
let(:context) do
GraphQL::Query::Context.new(
@@ -39,174 +40,208 @@ RSpec.describe Mutations::Commits::Create do
let(:mutated_commit) { subject[:commit] }
- it 'raises an error if the resource is not accessible to the user' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
-
- context 'when user does not have enough permissions' do
- before do
- project.add_guest(user)
- end
-
+ context 'when user is not a project member' do
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
- context 'when user is a maintainer of a different project' do
- before do
- create(:project_empty_repo).add_maintainer(user)
- end
+ context 'when user is a direct project member' do
+ context 'and user is a guest' do
+ before do
+ project.add_guest(user)
+ end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
- end
- context 'when the user can create a commit' do
- let(:deltas) { mutated_commit.raw_deltas }
+ context 'and user is a developer' do
+ let(:deltas) { mutated_commit.raw_deltas }
- before_all do
- project.add_developer(user)
- end
-
- context 'when service successfully creates a new commit' do
- it "returns the ETag path for the commit's pipeline" do
- commit_pipeline_path = subject[:commit_pipeline_path]
- expect(commit_pipeline_path).to match(%r(pipelines/sha/\w+))
+ before_all do
+ project.add_developer(user)
end
- it 'returns the content of the commit' do
- expect(subject[:content]).to eq(actions.pluck(:content))
- end
+ context 'when service successfully creates a new commit' do
+ it "returns the ETag path for the commit's pipeline" do
+ commit_pipeline_path = subject[:commit_pipeline_path]
+ expect(commit_pipeline_path).to match(%r(pipelines/sha/\w+))
+ end
- it 'returns a new commit' do
- expect(mutated_commit).to have_attributes(message: message, project: project)
- expect(subject[:errors]).to be_empty
+ it 'returns the content of the commit' do
+ expect(subject[:content]).to eq(actions.pluck(:content))
+ end
- expect_to_contain_deltas([
- a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: file_path)
- ])
+ it 'returns a new commit' do
+ expect(mutated_commit).to have_attributes(message: message, project: project)
+ expect(subject[:errors]).to be_empty
+
+ expect_to_contain_deltas([
+ a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: file_path)
+ ])
+ end
end
- end
- context 'when request has multiple actions' do
- let(:actions) do
- [
- {
- action: 'create',
- file_path: 'foo/foobar',
- content: 'some content'
- },
- {
- action: 'delete',
- file_path: 'README.md'
- },
- {
- action: 'move',
- file_path: "LICENSE.md",
- previous_path: "LICENSE",
- content: "some content"
- },
- {
- action: 'update',
- file_path: 'VERSION',
- content: 'new content'
- },
- {
- action: 'chmod',
- file_path: 'CHANGELOG',
- execute_filemode: true
- }
- ]
+ context 'when request has multiple actions' do
+ let(:actions) do
+ [
+ {
+ action: 'create',
+ file_path: 'foo/foobar',
+ content: 'some content'
+ },
+ {
+ action: 'delete',
+ file_path: 'README.md'
+ },
+ {
+ action: 'move',
+ file_path: "LICENSE.md",
+ previous_path: "LICENSE",
+ content: "some content"
+ },
+ {
+ action: 'update',
+ file_path: 'VERSION',
+ content: 'new content'
+ },
+ {
+ action: 'chmod',
+ file_path: 'CHANGELOG',
+ execute_filemode: true
+ }
+ ]
+ end
+
+ it 'returns a new commit' do
+ expect(mutated_commit).to have_attributes(message: message, project: project)
+ expect(subject[:errors]).to be_empty
+
+ expect_to_contain_deltas([
+ a_hash_including(a_mode: '0', b_mode: '100644', new_path: 'foo/foobar'),
+ a_hash_including(deleted_file: true, new_path: 'README.md'),
+ a_hash_including(deleted_file: true, new_path: 'LICENSE'),
+ a_hash_including(new_file: true, new_path: 'LICENSE.md'),
+ a_hash_including(new_file: false, new_path: 'VERSION'),
+ a_hash_including(a_mode: '100644', b_mode: '100755', new_path: 'CHANGELOG')
+ ])
+ end
end
- it 'returns a new commit' do
- expect(mutated_commit).to have_attributes(message: message, project: project)
- expect(subject[:errors]).to be_empty
-
- expect_to_contain_deltas([
- a_hash_including(a_mode: '0', b_mode: '100644', new_path: 'foo/foobar'),
- a_hash_including(deleted_file: true, new_path: 'README.md'),
- a_hash_including(deleted_file: true, new_path: 'LICENSE'),
- a_hash_including(new_file: true, new_path: 'LICENSE.md'),
- a_hash_including(new_file: false, new_path: 'VERSION'),
- a_hash_including(a_mode: '100644', b_mode: '100755', new_path: 'CHANGELOG')
- ])
+ context 'when actions are not defined' do
+ let(:actions) { [] }
+
+ it 'returns a new commit' do
+ expect(mutated_commit).to have_attributes(message: message, project: project)
+ expect(subject[:errors]).to be_empty
+
+ expect_to_contain_deltas([])
+ end
end
- end
- context 'when actions are not defined' do
- let(:actions) { [] }
+ context 'when branch does not exist' do
+ let(:branch) { 'unknown' }
- it 'returns a new commit' do
- expect(mutated_commit).to have_attributes(message: message, project: project)
- expect(subject[:errors]).to be_empty
+ it 'returns errors' do
+ expect(mutated_commit).to be_nil
+ expect(subject[:errors]).to match_array(['You can only create or edit files when you are on a branch'])
+ end
+ end
- expect_to_contain_deltas([])
+ context 'when branch does not exist and a start branch is provided' do
+ let(:branch) { 'my-branch' }
+ let(:start_branch) { 'master' }
+ let(:actions) do
+ [
+ {
+ action: 'create',
+ file_path: 'ANOTHER_FILE.md',
+ content: 'Bye'
+ }
+ ]
+ end
+
+ it 'returns a new commit' do
+ expect(mutated_commit).to have_attributes(message: message, project: project)
+ expect(subject[:errors]).to be_empty
+ expect(subject[:content]).to eq(actions.pluck(:content))
+
+ expect_to_contain_deltas([
+ a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: 'ANOTHER_FILE.md')
+ ])
+ end
end
- end
- context 'when branch does not exist' do
- let(:branch) { 'unknown' }
+ context 'when message is not set' do
+ let(:message) { nil }
- it 'returns errors' do
- expect(mutated_commit).to be_nil
- expect(subject[:errors]).to eq(['You can only create or edit files when you are on a branch'])
+ it 'returns errors' do
+ expect(mutated_commit).to be_nil
+ expect(subject[:errors].to_s).to match(/3:UserCommitFiles: empty CommitMessage/)
+ end
end
- end
- context 'when branch does not exist and a start branch is provided' do
- let(:branch) { 'my-branch' }
- let(:start_branch) { 'master' }
- let(:actions) do
- [
- {
- action: 'create',
- file_path: 'ANOTHER_FILE.md',
- content: 'Bye'
- }
- ]
+ context 'when actions are incorrect' do
+ let(:actions) { [{ action: 'unknown', file_path: 'test.md', content: '' }] }
+
+ it 'returns errors' do
+ expect(mutated_commit).to be_nil
+ expect(subject[:errors]).to match_array(['Unknown action \'unknown\''])
+ end
end
- it 'returns a new commit' do
- expect(mutated_commit).to have_attributes(message: message, project: project)
- expect(subject[:errors]).to be_empty
- expect(subject[:content]).to eq(actions.pluck(:content))
+ context 'when branch is protected' do
+ before do
+ create(:protected_branch, project: project, name: branch)
+ end
- expect_to_contain_deltas([
- a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: 'ANOTHER_FILE.md')
- ])
+ it 'returns errors' do
+ expect(mutated_commit).to be_nil
+ expect(subject[:errors]).to match_array(['You are not allowed to push into this branch'])
+ end
end
end
+ end
- context 'when message is not set' do
- let(:message) { nil }
+ context 'when user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- it 'returns errors' do
- expect(mutated_commit).to be_nil
- expect(subject[:errors].to_s).to match(/3:UserCommitFiles: empty CommitMessage/)
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
end
- context 'when actions are incorrect' do
- let(:actions) { [{ action: 'unknown', file_path: 'test.md', content: '' }] }
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ end
- it 'returns errors' do
- expect(mutated_commit).to be_nil
- expect(subject[:errors]).to eq(['Unknown action \'unknown\''])
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
end
+ end
- context 'when branch is protected' do
- before do
- create(:protected_branch, project: project, name: branch)
- end
+ context 'when user is a maintainer of a different project' do
+ before do
+ create(:project_empty_repo).add_maintainer(user)
+ end
- it 'returns errors' do
- expect(mutated_commit).to be_nil
- expect(subject[:errors]).to eq(['You are not allowed to push into this branch'])
- end
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
diff --git a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
index b93fb36a8ff..2476431816e 100644
--- a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
+++ b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -43,11 +43,12 @@ RSpec.describe Mutations::Environments::CanaryIngress::Update do
end
it 'returns notice about feature removal' do
- expect(subject[:errors]).to match_array([
- 'This endpoint was deactivated as part of the certificate-based' \
- 'kubernetes integration removal. See Epic:' \
- 'https://gitlab.com/groups/gitlab-org/configure/-/epics/8'
- ])
+ expect(subject[:errors]).to match_array(
+ [
+ 'This endpoint was deactivated as part of the certificate-based' \
+ 'kubernetes integration removal. See Epic:' \
+ 'https://gitlab.com/groups/gitlab-org/configure/-/epics/8'
+ ])
end
end
end
diff --git a/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
index 4541f8af7d3..56514c985ff 100644
--- a/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
+++ b/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::TimelineEvent::PromoteFromNote do
+ include NotesHelper
+
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
@@ -23,7 +25,7 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::PromoteFromNote do
let(:expected_timeline_event) do
instance_double(
'IncidentManagement::TimelineEvent',
- note: comment.note,
+ note: "@#{comment.author.username} [commented](#{noteable_note_url(comment)}): '#{comment.note}'",
occurred_at: comment.created_at.to_s,
incident: incident,
author: current_user,
diff --git a/spec/graphql/mutations/merge_requests/create_spec.rb b/spec/graphql/mutations/merge_requests/create_spec.rb
index e1edb60e4ff..6e593a5f4be 100644
--- a/spec/graphql/mutations/merge_requests/create_spec.rb
+++ b/spec/graphql/mutations/merge_requests/create_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Mutations::MergeRequests::Create do
subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
- let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:user) { create(:user) }
+ let(:user) { create(:user) }
let(:context) do
GraphQL::Query::Context.new(
@@ -38,62 +37,106 @@ RSpec.describe Mutations::MergeRequests::Create do
let(:mutated_merge_request) { subject[:merge_request] }
- it 'raises an error if the resource is not accessible to the user' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ shared_examples 'resource not available' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
- context 'when user does not have enough permissions to create a merge request' do
- before do
- project.add_guest(user)
- end
+ context 'when user is not a project member' do
+ let_it_be(:project) { create(:project, :public, :repository) }
- it 'raises an error if the resource is not accessible to the user' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
+ it_behaves_like 'resource not available'
end
- context 'when the user can create a merge request' do
- before_all do
- project.add_developer(user)
- end
+ context 'when user is a direct project member' do
+ let_it_be(:project) { create(:project, :public, :repository) }
- it 'creates a new merge request' do
- expect { mutated_merge_request }.to change(MergeRequest, :count).by(1)
- end
+ context 'and user is a guest' do
+ before do
+ project.add_guest(user)
+ end
- it 'returns a new merge request' do
- expect(mutated_merge_request.title).to eq(title)
- expect(subject[:errors]).to be_empty
+ it_behaves_like 'resource not available'
end
- context 'when optional description field is set' do
- let(:description) { 'content' }
+ context 'and user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates a new merge request' do
+ expect { mutated_merge_request }.to change(MergeRequest, :count).by(1)
+ end
- it 'returns a new merge request with a description' do
- expect(mutated_merge_request.description).to eq(description)
+ it 'returns a new merge request' do
+ expect(mutated_merge_request.title).to eq(title)
expect(subject[:errors]).to be_empty
end
- end
- context 'when optional labels field is set' do
- let(:labels) { %w[label-1 label-2] }
+ context 'when optional description field is set' do
+ let(:description) { 'content' }
- it 'returns a new merge request with labels' do
- expect(mutated_merge_request.labels.map(&:title)).to eq(labels)
- expect(subject[:errors]).to be_empty
+ it 'returns a new merge request with a description' do
+ expect(mutated_merge_request.description).to eq(description)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when optional labels field is set' do
+ let(:labels) { %w[label-1 label-2] }
+
+ it 'returns a new merge request with labels' do
+ expect(mutated_merge_request.labels.map(&:title)).to eq(labels)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when service cannot create a merge request' do
+ let(:title) { nil }
+
+ it 'does not create a new merge request' do
+ expect { mutated_merge_request }.not_to change(MergeRequest, :count)
+ end
+
+ it 'returns errors' do
+ expect(mutated_merge_request).to be_nil
+ expect(subject[:errors]).to match_array(['Title can\'t be blank'])
+ end
end
end
+ end
- context 'when service cannot create a merge request' do
- let(:title) { nil }
+ context 'when user is an inherited member from the group' do
+ let_it_be(:group) { create(:group, :public) }
- it 'does not create a new merge request' do
- expect { mutated_merge_request }.not_to change(MergeRequest, :count)
+ context 'when project is public with private merge requests' do
+ let_it_be(:project) do
+ create(:project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED)
end
- it 'returns errors' do
- expect(mutated_merge_request).to be_nil
- expect(subject[:errors]).to eq(['Title can\'t be blank'])
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ end
+
+ it_behaves_like 'resource not available'
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ before do
+ group.add_guest(user)
+ end
+
+ it_behaves_like 'resource not available'
end
end
end
diff --git a/spec/graphql/mutations/releases/create_spec.rb b/spec/graphql/mutations/releases/create_spec.rb
index b6b9449aa39..e7c25a20bad 100644
--- a/spec/graphql/mutations/releases/create_spec.rb
+++ b/spec/graphql/mutations/releases/create_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe Mutations::Releases::Create do
it 'has an access error' do
subject
- expect(resolve).to include(errors: ['Access Denied'])
+ expect(resolve).to include(errors: ['You are not allowed to create this tag as it is protected.'])
end
end
end
diff --git a/spec/graphql/mutations/releases/update_spec.rb b/spec/graphql/mutations/releases/update_spec.rb
index 15b10ea0648..0cf10e03fb1 100644
--- a/spec/graphql/mutations/releases/update_spec.rb
+++ b/spec/graphql/mutations/releases/update_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Mutations::Releases::Update do
let_it_be(:release) do
create(:release, project: project, tag: tag, name: name,
- description: description, released_at: released_at,
- created_at: created_at, milestones: [milestone_12_3, milestone_12_4])
+ description: description, released_at: released_at,
+ created_at: created_at, milestones: [milestone_12_3, milestone_12_4])
end
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
diff --git a/spec/graphql/resolvers/board_lists_resolver_spec.rb b/spec/graphql/resolvers/board_lists_resolver_spec.rb
index c882ad7c818..2fb7c5c4717 100644
--- a/spec/graphql/resolvers/board_lists_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_lists_resolver_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Resolvers::BoardListsResolver do
def resolve_board_lists(args: {}, current_user: user)
resolve(described_class, obj: board, args: args, ctx: { current_user: current_user },
- arg_style: :internal
+ arg_style: :internal
)
end
end
diff --git a/spec/graphql/resolvers/ci/config_resolver_spec.rb b/spec/graphql/resolvers/ci/config_resolver_spec.rb
index dc030b1313b..692bdf58784 100644
--- a/spec/graphql/resolvers/ci/config_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/config_resolver_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Resolvers::Ci::ConfigResolver do
subject(:response) do
resolve(described_class,
args: { project_path: project.full_path, content: content, sha: sha },
- ctx: { current_user: user })
+ ctx: { current_user: user })
end
shared_examples 'a valid config file' do
diff --git a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
index f99f48f5b07..57b2fcbea63 100644
--- a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Resolvers::Ci::GroupRunnersResolver do
describe '#resolve' do
subject do
resolve(described_class, obj: obj, ctx: { current_user: user }, args: args,
- arg_style: :internal)
+ arg_style: :internal)
end
include_context 'runners resolver setup'
diff --git a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
index ac7cef20df4..1bfd6fbf6b9 100644
--- a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
@@ -20,10 +20,10 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
project.add_member(current_user, :maintainer)
end
- it 'returns nil when scope is not enabled' do
+ it 'returns the same project in the allow list of projects for the Ci Job Token when scope is not enabled' do
allow(project).to receive(:ci_job_token_scope_enabled?).and_return(false)
- expect(resolve_scope).to eq(nil)
+ expect(resolve_scope.all_projects).to contain_exactly(project)
end
it 'returns the same project in the allow list of projects for the Ci Job Token' do
@@ -43,8 +43,8 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
project.update!(ci_job_token_scope_enabled: false)
end
- it 'returns nil' do
- expect(resolve_scope).to be_nil
+ it 'resolves projects' do
+ expect(resolve_scope.all_projects).to contain_exactly(project)
end
end
end
diff --git a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb
index 6c228861ddf..80a70938dc4 100644
--- a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Resolvers::Ci::JobsResolver do
::Types::Security::ReportTypeEnum.values['DAST'].value
]
jobs = resolve(described_class, obj: pipeline, args: { security_report_types: report_types },
- arg_style: :internal)
+ arg_style: :internal)
expect(jobs).to contain_exactly(
have_attributes(name: 'DAST job'),
diff --git a/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb
new file mode 100644
index 00000000000..952c7337d65
--- /dev/null
+++ b/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::RunnerProjectsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project1) { create(:project, description: 'Project1.1') }
+ let_it_be(:project2) { create(:project, description: 'Project1.2') }
+ let_it_be(:project3) { create(:project, description: 'Project2.1') }
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project1, project2, project3]) }
+
+ let(:args) { {} }
+
+ subject { resolve_projects(args) }
+
+ describe '#resolve' do
+ context 'with authorized user', :enable_admin_mode do
+ let(:current_user) { create(:user, :admin) }
+
+ context 'with search argument' do
+ let(:args) { { search: 'Project1.' } }
+
+ it 'returns a lazy value with projects containing the specified prefix' do
+ expect(subject).to be_a(GraphQL::Execution::Lazy)
+ expect(subject.value).to contain_exactly(project1, project2)
+ end
+ end
+
+ context 'with supported arguments' do
+ let(:args) { { membership: true, search_namespaces: true, topics: %w[xyz] } }
+
+ it 'creates ProjectsFinder with expected arguments' do
+ expect(ProjectsFinder).to receive(:new).with(
+ a_hash_including(
+ params: a_hash_including(
+ non_public: true,
+ search_namespaces: true,
+ topic: %w[xyz]
+ )
+ )
+ ).and_call_original
+
+ expect(subject).to be_a(GraphQL::Execution::Lazy)
+ subject.value
+ end
+ end
+
+ context 'without arguments' do
+ it 'returns a lazy value with all projects' do
+ expect(subject).to be_a(GraphQL::Execution::Lazy)
+ expect(subject.value).to contain_exactly(project1, project2, project3)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ private
+
+ def resolve_projects(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: runner, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index 8586d359336..4038192a68a 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
subject do
resolve(described_class, obj: obj, ctx: { current_user: user }, args: args,
- arg_style: :internal)
+ arg_style: :internal)
end
include_context 'runners resolver setup'
diff --git a/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
index 606c6eb03a3..4083e77a38f 100644
--- a/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
@@ -32,11 +32,12 @@ RSpec.describe Resolvers::Ci::TestSuiteResolver do
# Each test failure in this pipeline has a matching failure in the default branch
recent_failures = test_suite[:test_cases].map { |tc| tc[:recent_failures] }
- expect(recent_failures).to eq([
- { count: 1, base_branch: 'master' },
- { count: 1, base_branch: 'master' },
- { count: 1, base_branch: 'master' }
- ])
+ expect(recent_failures).to eq(
+ [
+ { count: 1, base_branch: 'master' },
+ { count: 1, base_branch: 'master' },
+ { count: 1, base_branch: 'master' }
+ ])
end
end
diff --git a/spec/graphql/resolvers/container_repositories_resolver_spec.rb b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
index ed922259903..8cbb366f873 100644
--- a/spec/graphql/resolvers/container_repositories_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Resolvers::ContainerRepositoriesResolver do
subject do
resolve(described_class, ctx: { current_user: user }, args: args, obj: object,
- arg_style: :internal)
+ arg_style: :internal)
end
shared_examples 'returning container repositories' do
diff --git a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
index 9747f663759..3ed3fe76267 100644
--- a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
describe '#resolve' do
let(:resolver) do
resolve(described_class, ctx: { current_user: user }, obj: repository, args: args,
- arg_style: :internal)
+ arg_style: :internal)
end
before do
diff --git a/spec/graphql/resolvers/crm/organization_state_counts_resolver_spec.rb b/spec/graphql/resolvers/crm/organization_state_counts_resolver_spec.rb
new file mode 100644
index 00000000000..c6ad4beeee0
--- /dev/null
+++ b/spec/graphql/resolvers/crm/organization_state_counts_resolver_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Crm::OrganizationStateCountsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+
+ before_all do
+ create(:organization, group: group, name: "ABC Corp")
+ create(:organization, group: group, name: "123 Corp", state: 'inactive')
+ create_list(:organization, 3, group: group)
+ create_list(:organization, 2, group: group, state: 'inactive')
+ end
+
+ describe '#resolve' do
+ context 'with unauthorized user' do
+ it 'does not raise an error and returns nil' do
+ expect { resolve_counts(group) }.not_to raise_error
+ expect(resolve_counts(group)).to be_nil
+ end
+ end
+
+ context 'with authorized user' do
+ before do
+ group.add_reporter(user)
+ end
+
+ context 'without parent' do
+ it 'returns nil' do
+ expect(resolve_counts(nil)).to be_nil
+ end
+ end
+
+ context 'with a group' do
+ context 'when no filter is provided' do
+ it 'returns the count of all organizations' do
+ counts = resolve_counts(group)
+ expect(counts['active']).to eq(4)
+ expect(counts['inactive']).to eq(3)
+ end
+ end
+
+ context 'when search term is provided' do
+ it 'returns the correct counts' do
+ counts = resolve_counts(group, { search: "Corp" })
+
+ expect(counts['active']).to eq(1)
+ expect(counts['inactive']).to eq(1)
+ end
+ end
+ end
+ end
+ end
+
+ def resolve_counts(parent, args = {}, context = { current_user: user })
+ resolve(described_class, obj: parent, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/crm/organizations_resolver_spec.rb b/spec/graphql/resolvers/crm/organizations_resolver_spec.rb
index 323f134ffc3..d5980bf3c41 100644
--- a/spec/graphql/resolvers/crm/organizations_resolver_spec.rb
+++ b/spec/graphql/resolvers/crm/organizations_resolver_spec.rb
@@ -55,11 +55,23 @@ RSpec.describe Resolvers::Crm::OrganizationsResolver do
end
context 'when no filter is provided' do
- it 'returns all the organizations in the correct order' do
+ it 'returns all the organizations in the default order' do
expect(resolve_organizations(group)).to eq([organization_a, organization_b])
end
end
+ context 'when a sort is provided' do
+ it 'returns all the organizations in the correct order' do
+ expect(resolve_organizations(group, { sort: 'NAME_DESC' })).to eq([organization_b, organization_a])
+ end
+ end
+
+ context 'when filtering for all states' do
+ it 'returns all the organizations' do
+ expect(resolve_organizations(group, { state: 'all' })).to contain_exactly(organization_a, organization_b)
+ end
+ end
+
context 'when search term is provided' do
it 'returns the correct organizations' do
expect(resolve_organizations(group, { search: "def" })).to contain_exactly(organization_b)
diff --git a/spec/graphql/resolvers/deployment_resolver_spec.rb b/spec/graphql/resolvers/deployment_resolver_spec.rb
new file mode 100644
index 00000000000..9231edefddc
--- /dev/null
+++ b/spec/graphql/resolvers/deployment_resolver_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::DeploymentResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let(:current_user) { developer }
+
+ describe '#resolve' do
+ it 'finds the deployment' do
+ expect(resolve_deployments(iid: deployment.iid)).to contain_exactly(deployment)
+ end
+
+ it 'does not find the deployment if the IID does not match' do
+ expect(resolve_deployments(iid: non_existing_record_id)).to be_empty
+ end
+ end
+
+ def resolve_deployments(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/deployments_resolver_spec.rb b/spec/graphql/resolvers/deployments_resolver_spec.rb
new file mode 100644
index 00000000000..4e5564aad0b
--- /dev/null
+++ b/spec/graphql/resolvers/deployments_resolver_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::DeploymentsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let(:current_user) { developer }
+
+ describe '#resolve' do
+ it 'finds the deployment' do
+ expect(resolve_deployments).to contain_exactly(deployment)
+ end
+
+ it 'finds the deployment when status matches' do
+ expect(resolve_deployments(statuses: [:created])).to contain_exactly(deployment)
+ end
+
+ it 'does not find the deployment when status does not match' do
+ expect(resolve_deployments(statuses: [:success])).to be_empty
+ end
+
+ it 'transforms order_by for finder' do
+ expect(DeploymentsFinder)
+ .to receive(:new)
+ .with(environment: environment.id, status: ['success'], order_by: 'finished_at', sort: 'asc')
+ .and_call_original
+
+ resolve_deployments(statuses: [:success], order_by: { finished_at: :asc })
+ end
+ end
+
+ def resolve_deployments(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: environment, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
index 3a2ed445484..eb39e5bafc5 100644
--- a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do
let(:params) do
{
earlier_or_equal_to_sha: first_version.sha,
- earlier_or_equal_to_id: global_id_of(first_version)
+ earlier_or_equal_to_id: global_id_of(first_version)
}
end
@@ -95,7 +95,7 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do
let(:params) do
{
earlier_or_equal_to_sha: first_version.sha,
- earlier_or_equal_to_id: global_id_of(other_version)
+ earlier_or_equal_to_id: global_id_of(other_version)
}
end
diff --git a/spec/graphql/resolvers/environments/last_deployment_resolver_spec.rb b/spec/graphql/resolvers/environments/last_deployment_resolver_spec.rb
new file mode 100644
index 00000000000..95a1a06730d
--- /dev/null
+++ b/spec/graphql/resolvers/environments/last_deployment_resolver_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Environments::LastDeploymentResolver do
+ include GraphqlHelpers
+ include Gitlab::Graphql::Laziness
+
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let(:current_user) { developer }
+
+ describe '#resolve' do
+ it 'finds the deployment when status matches' do
+ expect(resolve_last_deployment(status: :created)).to eq(deployment)
+ end
+
+ it 'does not find the deployment when status does not match' do
+ expect(resolve_last_deployment(status: :success)).to be_nil
+ end
+
+ it 'raises an error when status is not specified' do
+ expect { resolve_last_deployment }.to raise_error(ArgumentError)
+ end
+
+ it 'raises an error when status is not supported' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError,
+ '"skipped" status is not supported.') do
+ resolve_last_deployment(status: :skipped)
+ end
+ end
+ end
+
+ def resolve_last_deployment(args = {}, context = { current_user: current_user })
+ force(resolve(described_class, obj: environment, ctx: context, args: args))
+ end
+end
diff --git a/spec/graphql/resolvers/group_members_resolver_spec.rb b/spec/graphql/resolvers/group_members_resolver_spec.rb
index bd0b4870062..d860b87875e 100644
--- a/spec/graphql/resolvers/group_members_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_members_resolver_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe Resolvers::GroupMembersResolver do
+RSpec.describe 'Resolvers::GroupMembersResolver' do
include GraphqlHelpers
+ let(:described_class) { Resolvers::GroupMembersResolver }
+
specify do
expect(described_class).to have_nullable_graphql_type(Types::GroupMemberType.connection_type)
end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 89e45810033..a74b2a3f18c 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -311,49 +311,15 @@ RSpec.describe Resolvers::IssuesResolver do
end
context 'when searching issues' do
- it 'returns correct issues' do
- expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
- end
-
- it 'uses project search optimization' do
- expected_arguments = a_hash_including(
- search: 'foo',
- attempt_project_search_optimizations: true
- )
- expect(IssuesFinder).to receive(:new).with(anything, expected_arguments).and_call_original
-
- resolve_issues(search: 'foo')
- end
-
- context 'with anonymous user' do
- let_it_be(:public_project) { create(:project, :public) }
- let_it_be(:public_issue) { create(:issue, project: public_project, title: 'Test issue') }
-
- context 'with disable_anonymous_search enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'generates an error' do
- error_message = "User must be authenticated to include the `search` argument."
-
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, error_message) do
- resolve(described_class, obj: public_project, args: { search: 'test' }, ctx: { current_user: nil })
- end
- end
- end
-
- context 'with disable_anonymous_search disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
-
- it 'returns correct issues' do
- expect(
- resolve(described_class, obj: public_project, args: { search: 'test' }, ctx: { current_user: nil })
- ).to contain_exactly(public_issue)
- end
- end
+ it_behaves_like 'graphql query for searching issuables' do
+ let_it_be(:parent) { project }
+ let_it_be(:issuable1) { create(:issue, project: project, title: 'first created') }
+ let_it_be(:issuable2) { create(:issue, project: project, title: 'second created', description: 'text 1') }
+ let_it_be(:issuable3) { create(:issue, project: project, title: 'third', description: 'text 2') }
+ let_it_be(:issuable4) { create(:issue, project: project) }
+
+ let_it_be(:finder_class) { IssuesFinder }
+ let_it_be(:optimization_param) { :attempt_project_search_optimizations }
end
end
diff --git a/spec/graphql/resolvers/project_members_resolver_spec.rb b/spec/graphql/resolvers/project_members_resolver_spec.rb
index 2f4145b3215..c38cb3d157b 100644
--- a/spec/graphql/resolvers/project_members_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_members_resolver_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe Resolvers::ProjectMembersResolver do
+RSpec.describe 'Resolvers::ProjectMembersResolver' do
include GraphqlHelpers
+ let(:described_class) { Resolvers::ProjectMembersResolver }
+
it_behaves_like 'querying members with a group' do
let_it_be(:project) { create(:project, group: group_1) }
let_it_be(:resource_member) { create(:project_member, user: user_1, project: project) }
diff --git a/spec/graphql/resolvers/work_items_resolver_spec.rb b/spec/graphql/resolvers/work_items_resolver_spec.rb
index 29eac0ab46e..d89ccc7f806 100644
--- a/spec/graphql/resolvers/work_items_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_items_resolver_spec.rb
@@ -19,13 +19,13 @@ RSpec.describe Resolvers::WorkItemsResolver do
let_it_be(:item2) do
create(:work_item, project: project, state: :closed, title: 'foo',
- created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at:
+ created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at:
1.hour.ago)
end
let_it_be(:item3) do
create(:work_item, project: other_project, state: :closed, title: 'foo',
- created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at:
+ created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at:
1.hour.ago)
end
@@ -52,49 +52,15 @@ RSpec.describe Resolvers::WorkItemsResolver do
end
context 'when searching items' do
- it 'returns correct items' do
- expect(resolve_items(search: 'foo')).to contain_exactly(item2)
- end
-
- it 'uses project search optimization' do
- expected_arguments = a_hash_including(
- search: 'foo',
- attempt_project_search_optimizations: true
- )
- expect(::WorkItems::WorkItemsFinder).to receive(:new).with(anything, expected_arguments).and_call_original
-
- resolve_items(search: 'foo')
- end
-
- context 'with anonymous user' do
- let_it_be(:public_project) { create(:project, :public) }
- let_it_be(:public_item) { create(:work_item, project: public_project, title: 'Test item') }
-
- context 'with disable_anonymous_search enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'generates an error' do
- error_message = "User must be authenticated to include the `search` argument."
-
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, error_message) do
- resolve(described_class, obj: public_project, args: { search: 'test' }, ctx: { current_user: nil })
- end
- end
- end
-
- context 'with disable_anonymous_search disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
-
- it 'returns correct items' do
- expect(
- resolve(described_class, obj: public_project, args: { search: 'test' }, ctx: { current_user: nil })
- ).to contain_exactly(public_item)
- end
- end
+ it_behaves_like 'graphql query for searching issuables' do
+ let_it_be(:parent) { project }
+ let_it_be(:issuable1) { create(:work_item, project: project, title: 'first created') }
+ let_it_be(:issuable2) { create(:work_item, project: project, title: 'second created', description: 'text 1') }
+ let_it_be(:issuable3) { create(:work_item, project: project, title: 'third', description: 'text 2') }
+ let_it_be(:issuable4) { create(:work_item, project: project) }
+
+ let_it_be(:finder_class) { ::WorkItems::WorkItemsFinder }
+ let_it_be(:optimization_param) { :attempt_project_search_optimizations }
end
end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
index b85716e4d21..9f8a8717efb 100644
--- a/spec/graphql/types/base_field_spec.rb
+++ b/spec/graphql/types/base_field_spec.rb
@@ -205,39 +205,6 @@ RSpec.describe Types::BaseField do
end
end
end
-
- describe '#visible?' do
- context 'and has a feature_flag' do
- let(:flag) { :test_feature }
- let(:field) { described_class.new(name: 'test', type: GraphQL::Types::String, _deprecated_feature_flag: flag, null: false) }
- let(:context) { {} }
-
- before do
- skip_feature_flags_yaml_validation
- end
-
- it 'checks YAML definition for default_enabled' do
- # Exception is indicative of a check for YAML definition
- expect { field.visible?(context) }.to raise_error(Feature::InvalidFeatureFlagError, /The feature flag YAML definition for '#{flag}' does not exist/)
- end
-
- context 'skipping YAML check' do
- before do
- skip_default_enabled_yaml_check
- end
-
- it 'returns false if the feature is not enabled' do
- stub_feature_flags(flag => false)
-
- expect(field.visible?(context)).to eq(false)
- end
-
- it 'returns true if the feature is enabled' do
- expect(field.visible?(context)).to eq(true)
- end
- end
- end
- end
end
describe '#resolve' do
@@ -251,77 +218,11 @@ RSpec.describe Types::BaseField do
end
end
- describe '#description' do
- context 'feature flag given' do
- let(:field) { described_class.new(name: 'test', type: GraphQL::Types::String, _deprecated_feature_flag: flag, null: false, description: 'Test description.') }
- let(:flag) { :test_flag }
-
- it 'prepends the description' do
- expect(field.description).to start_with 'Test description. Available only when feature flag `test_flag` is enabled.'
- end
-
- context 'falsey feature_flag values' do
- using RSpec::Parameterized::TableSyntax
-
- where(:flag, :feature_value, :default_enabled) do
- '' | false | false
- '' | true | false
- nil | false | true
- nil | true | false
- end
-
- with_them do
- it 'returns the correct description' do
- expect(field.description).to eq('Test description.')
- end
- end
- end
-
- context 'with different default_enabled values' do
- using RSpec::Parameterized::TableSyntax
-
- where(:feature_value, :default_enabled, :expected_description) do
- disabled_ff_description = "Test description. Available only when feature flag `test_flag` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice."
- enabled_ff_description = "Test description. Available only when feature flag `test_flag` is enabled. This flag is enabled by default."
-
- false | false | disabled_ff_description
- true | false | disabled_ff_description
- false | true | enabled_ff_description
- true | true | enabled_ff_description
- end
-
- with_them do
- before do
- stub_feature_flags("#{flag}": feature_value)
-
- allow(Feature::Definition).to receive(:has_definition?).with(flag).and_return(true)
- allow(Feature::Definition).to receive(:default_enabled?).and_return(default_enabled)
- end
-
- it 'returns the correct availability in the description' do
- expect(field.description).to eq expected_description
- end
- end
- end
- end
- end
-
include_examples 'Gitlab-style deprecations' do
def subject(args = {})
base_args = { name: 'test', type: GraphQL::Types::String, null: true }
described_class.new(**base_args.merge(args))
end
-
- it 'interacts well with the `_deprecated_feature_flag` property' do
- field = subject(
- deprecated: { milestone: '1.10', reason: 'Deprecation reason' },
- description: 'Field description.',
- _deprecated_feature_flag: 'foo_flag'
- )
-
- expect(field.description).to start_with('Field description. Available only when feature flag `foo_flag` is enabled.')
- expect(field.description).to end_with('Deprecated in 1.10: Deprecation reason.')
- end
end
end
diff --git a/spec/graphql/types/branch_protections/merge_access_level_type_spec.rb b/spec/graphql/types/branch_protections/merge_access_level_type_spec.rb
new file mode 100644
index 00000000000..8cc1005d97e
--- /dev/null
+++ b/spec/graphql/types/branch_protections/merge_access_level_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MergeAccessLevel'] do
+ subject { described_class }
+
+ let(:fields) { %i[access_level access_level_description] }
+
+ specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
+
+ specify { is_expected.to have_graphql_fields(fields).at_least }
+end
diff --git a/spec/graphql/types/branch_protections/push_access_level_type_spec.rb b/spec/graphql/types/branch_protections/push_access_level_type_spec.rb
new file mode 100644
index 00000000000..c78c0bda74c
--- /dev/null
+++ b/spec/graphql/types/branch_protections/push_access_level_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PushAccessLevel'] do
+ subject { described_class }
+
+ let(:fields) { %i[access_level access_level_description] }
+
+ specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
+
+ specify { is_expected.to have_graphql_fields(fields).at_least }
+end
diff --git a/spec/graphql/types/branch_rule_type_spec.rb b/spec/graphql/types/branch_rule_type_spec.rb
new file mode 100644
index 00000000000..277901f00bf
--- /dev/null
+++ b/spec/graphql/types/branch_rule_type_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['BranchRule'] do
+ include GraphqlHelpers
+
+ subject { described_class }
+
+ let(:fields) do
+ %i[
+ name
+ branch_protection
+ created_at
+ updated_at
+ ]
+ end
+
+ specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
+
+ specify { is_expected.to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/branch_rules/branch_protection_type_spec.rb b/spec/graphql/types/branch_rules/branch_protection_type_spec.rb
new file mode 100644
index 00000000000..bbc92fd8fef
--- /dev/null
+++ b/spec/graphql/types/branch_rules/branch_protection_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['BranchProtection'] do
+ subject { described_class }
+
+ let(:fields) { %i[merge_access_levels push_access_levels allow_force_push] }
+
+ specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
+
+ specify { is_expected.to have_graphql_fields(fields).at_least }
+end
diff --git a/spec/graphql/types/ci/config_variable_type_spec.rb b/spec/graphql/types/ci/config_variable_type_spec.rb
new file mode 100644
index 00000000000..2b0937a7858
--- /dev/null
+++ b/spec/graphql/types/ci/config_variable_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiConfigVariable'] do
+ specify { expect(described_class).to have_graphql_fields(:key, :description, :value).at_least }
+end
diff --git a/spec/graphql/types/ci/group_variable_connection_type_spec.rb b/spec/graphql/types/ci/group_variable_connection_type_spec.rb
new file mode 100644
index 00000000000..4a1fd490506
--- /dev/null
+++ b/spec/graphql/types/ci/group_variable_connection_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiGroupVariableConnection'] do
+ it 'has the expected fields' do
+ expected_fields = %i[limit page_info edges nodes]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/instance_variable_type_spec.rb b/spec/graphql/types/ci/instance_variable_type_spec.rb
index cf4aaed31f1..c77a4ac1dc4 100644
--- a/spec/graphql/types/ci/instance_variable_type_spec.rb
+++ b/spec/graphql/types/ci/instance_variable_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiInstanceVariable'] do
specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
- specify { expect(described_class).to have_graphql_fields(:masked, :protected).at_least }
+ specify { expect(described_class).to have_graphql_fields(:environment_scope, :masked, :protected).at_least }
end
diff --git a/spec/graphql/types/ci/job_artifact_type_spec.rb b/spec/graphql/types/ci/job_artifact_type_spec.rb
index 58b5f9cfcb7..3e054faf0c9 100644
--- a/spec/graphql/types/ci/job_artifact_type_spec.rb
+++ b/spec/graphql/types/ci/job_artifact_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiJobArtifact'] do
it 'has the correct fields' do
- expected_fields = [:download_path, :file_type, :name]
+ expected_fields = [:id, :download_path, :file_type, :name, :size, :expire_at]
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/graphql/types/ci/job_token_scope_type_spec.rb b/spec/graphql/types/ci/job_token_scope_type_spec.rb
index 457d46b6896..18f4d762d1e 100644
--- a/spec/graphql/types/ci/job_token_scope_type_spec.rb
+++ b/spec/graphql/types/ci/job_token_scope_type_spec.rb
@@ -69,8 +69,8 @@ RSpec.describe GitlabSchema.types['CiJobTokenScopeType'] do
expect(subject['errors']).to be_nil
end
- it 'returns nil' do
- expect(subject['data']['project']['ciJobTokenScope']).to be_nil
+ it 'returns readable projects in scope' do
+ expect(returned_project_paths).to contain_exactly(project.path)
end
end
end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index bc9e64282bc..b3dee082d1f 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Types::Ci::JobType do
+ include GraphqlHelpers
+
specify { expect(described_class.graphql_name).to eq('CiJob') }
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Job) }
@@ -45,8 +47,21 @@ RSpec.describe Types::Ci::JobType do
tags
triggered
userPermissions
+ webPath
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
+
+ describe '#web_path' do
+ subject { resolve_field(:web_path, build, current_user: user, object_type: described_class) }
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, project: project, user: user) }
+
+ it 'returns the web path of the job' do
+ is_expected.to eq("/#{project.full_path}/-/jobs/#{build.id}")
+ end
+ end
end
diff --git a/spec/graphql/types/ci/manual_variable_type_spec.rb b/spec/graphql/types/ci/manual_variable_type_spec.rb
index 2884c818a52..21d36b7dfc0 100644
--- a/spec/graphql/types/ci/manual_variable_type_spec.rb
+++ b/spec/graphql/types/ci/manual_variable_type_spec.rb
@@ -4,4 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiManualVariable'] do
specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
+
+ specify { expect(described_class).to have_graphql_fields(:environment_scope).at_least }
end
diff --git a/spec/graphql/types/ci/project_variable_connection_type_spec.rb b/spec/graphql/types/ci/project_variable_connection_type_spec.rb
new file mode 100644
index 00000000000..97c3a207f7f
--- /dev/null
+++ b/spec/graphql/types/ci/project_variable_connection_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiProjectVariableConnection'] do
+ it 'has the expected fields' do
+ expected_fields = %i[limit page_info edges nodes]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/runner_architecture_type_spec.rb b/spec/graphql/types/ci/runner_architecture_type_spec.rb
index 527adef8cf9..60709acfd53 100644
--- a/spec/graphql/types/ci/runner_architecture_type_spec.rb
+++ b/spec/graphql/types/ci/runner_architecture_type_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe Types::Ci::RunnerArchitectureType do
it 'exposes the expected fields' do
expected_fields = %i[
- name
- download_location
+ name
+ download_location
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/runner_platform_type_spec.rb b/spec/graphql/types/ci/runner_platform_type_spec.rb
index 66b83f607d0..29b8e885183 100644
--- a/spec/graphql/types/ci/runner_platform_type_spec.rb
+++ b/spec/graphql/types/ci/runner_platform_type_spec.rb
@@ -7,9 +7,9 @@ RSpec.describe Types::Ci::RunnerPlatformType do
it 'exposes the expected fields' do
expected_fields = %i[
- name
- human_readable_name
- architectures
+ name
+ human_readable_name
+ architectures
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/variable_interface_spec.rb b/spec/graphql/types/ci/variable_interface_spec.rb
index 8cef0ac2a14..328c5305a44 100644
--- a/spec/graphql/types/ci/variable_interface_spec.rb
+++ b/spec/graphql/types/ci/variable_interface_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiVariable'] do
specify do
expect(described_class).to have_graphql_fields(
- :id, :key, :value, :variable_type, :raw
+ :id, :key, :raw, :value, :variable_type
).at_least
end
end
diff --git a/spec/graphql/types/clusters/agent_type_spec.rb b/spec/graphql/types/clusters/agent_type_spec.rb
index 3f4faccf15d..bb1006c55c0 100644
--- a/spec/graphql/types/clusters/agent_type_spec.rb
+++ b/spec/graphql/types/clusters/agent_type_spec.rb
@@ -9,5 +9,5 @@ RSpec.describe GitlabSchema.types['ClusterAgent'] do
it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
- it { expect(described_class).to have_graphql_fields(fields) }
+ it { expect(described_class).to include_graphql_fields(*fields) }
end
diff --git a/spec/graphql/types/customer_relations/organization_sort_enum_spec.rb b/spec/graphql/types/customer_relations/organization_sort_enum_spec.rb
new file mode 100644
index 00000000000..7ff498f0097
--- /dev/null
+++ b/spec/graphql/types/customer_relations/organization_sort_enum_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationSort'] do
+ specify { expect(described_class.graphql_name).to eq('OrganizationSort') }
+
+ it_behaves_like 'common sort values'
+
+ it 'exposes all the contact sort values' do
+ expect(described_class.values.keys).to include(
+ *%w[
+ NAME_ASC
+ NAME_DESC
+ DESCRIPTION_ASC
+ DESCRIPTION_DESC
+ DEFAULT_RATE_ASC
+ DEFAULT_RATE_DESC
+ ]
+ )
+ end
+end
diff --git a/spec/graphql/types/customer_relations/organization_state_counts_type_spec.rb b/spec/graphql/types/customer_relations/organization_state_counts_type_spec.rb
new file mode 100644
index 00000000000..a2c8dacb1a5
--- /dev/null
+++ b/spec/graphql/types/customer_relations/organization_state_counts_type_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationStateCounts'] do
+ include GraphqlHelpers
+
+ let(:fields) do
+ %w[
+ all
+ active
+ inactive
+ ]
+ end
+
+ let(:object) do
+ {
+ 'inactive' => 3,
+ 'active' => 4
+ }
+ end
+
+ it { expect(described_class.graphql_name).to eq('OrganizationStateCounts') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+
+ describe '#all' do
+ it 'returns the sum of all counts' do
+ expect(resolve_field(:all, object)).to eq(7)
+ end
+ end
+end
diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb
new file mode 100644
index 00000000000..70fdc38019e
--- /dev/null
+++ b/spec/graphql/types/deployment_details_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DeploymentDetails'] do
+ specify { expect(described_class.graphql_name).to eq('DeploymentDetails') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ id iid ref tag tags sha created_at updated_at finished_at status commit job triggerer
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
+end
diff --git a/spec/graphql/types/deployment_type_spec.rb b/spec/graphql/types/deployment_type_spec.rb
new file mode 100644
index 00000000000..bf4be0523c6
--- /dev/null
+++ b/spec/graphql/types/deployment_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Deployment'] do
+ specify { expect(described_class.graphql_name).to eq('Deployment') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ id iid ref tag sha created_at updated_at finished_at status commit job triggerer
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
+end
diff --git a/spec/graphql/types/detployment_tag_type_spec.rb b/spec/graphql/types/detployment_tag_type_spec.rb
new file mode 100644
index 00000000000..9a7a8db0970
--- /dev/null
+++ b/spec/graphql/types/detployment_tag_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DeploymentTag'] do
+ specify { expect(described_class.graphql_name).to eq('DeploymentTag') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ name path
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index 3671d35e8a5..ae58fe00af7 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do
expected_fields = %w[
- name id state metrics_dashboard latest_opened_most_severe_alert path
+ name id state metrics_dashboard latest_opened_most_severe_alert path external_url deployments
+ slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment
]
expect(described_class).to have_graphql_fields(*expected_fields)
@@ -17,7 +18,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
context 'when there is an environment' do
let_it_be(:project) { create(:project) }
- let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:environment) { create(:environment, project: project, external_url: 'https://gitlab.com') }
let_it_be(:user) { create(:user) }
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
@@ -29,6 +30,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
environment(name: "#{environment.name}") {
name
path
+ externalUrl
state
}
}
@@ -50,6 +52,10 @@ RSpec.describe GitlabSchema.types['Environment'] do
)
end
+ it 'returns the external url of the environment' do
+ expect(subject['data']['project']['environment']['externalUrl']).to eq(environment.external_url)
+ end
+
context 'when query alert data for the environment' do
let_it_be(:query) do
%(
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 72b3bb90194..0b65778ce90 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -24,8 +24,9 @@ RSpec.describe GitlabSchema.types['Group'] do
dependency_proxy_blobs dependency_proxy_image_count
dependency_proxy_blob_count dependency_proxy_total_size
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
- shared_runners_setting timelogs organizations contacts contact_state_counts
- work_item_types recent_issue_boards ci_variables
+ shared_runners_setting timelogs organization_state_counts organizations
+ contact_state_counts contacts work_item_types
+ recent_issue_boards ci_variables
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -62,6 +63,13 @@ RSpec.describe GitlabSchema.types['Group'] do
it { is_expected.to have_graphql_resolver(Resolvers::Crm::ContactStateCountsResolver) }
end
+ describe 'organization_state_counts field' do
+ subject { described_class.fields['organizationStateCounts'] }
+
+ it { is_expected.to have_graphql_type(Types::CustomerRelations::OrganizationStateCountsType) }
+ it { is_expected.to have_graphql_resolver(Resolvers::Crm::OrganizationStateCountsResolver) }
+ end
+
it_behaves_like 'a GraphQL type with labels' do
let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups, :includeDescendantGroups, :onlyGroupLabels] }
end
diff --git a/spec/graphql/types/merge_request_review_state_enum_spec.rb b/spec/graphql/types/merge_request_review_state_enum_spec.rb
index 407a1ae3c1f..486e1c4f502 100644
--- a/spec/graphql/types/merge_request_review_state_enum_spec.rb
+++ b/spec/graphql/types/merge_request_review_state_enum_spec.rb
@@ -12,10 +12,6 @@ RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do
'UNREVIEWED' => have_attributes(
description: 'The merge request is unreviewed.',
value: 'unreviewed'
- ),
- 'ATTENTION_REQUESTED' => have_attributes(
- description: 'The merge request is attention_requested.',
- value: 'attention_requested'
)
)
end
diff --git a/spec/graphql/types/metrics/dashboard_type_spec.rb b/spec/graphql/types/metrics/dashboard_type_spec.rb
index 30dccc7c0be..114db87d5f1 100644
--- a/spec/graphql/types/metrics/dashboard_type_spec.rb
+++ b/spec/graphql/types/metrics/dashboard_type_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe GitlabSchema.types['MetricsDashboard'] do
it 'has the expected fields' do
expected_fields = %w[
- path annotations schema_validation_warnings
- ]
+ path annotations schema_validation_warnings
+ ]
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/graphql/types/packages/composer/metadatum_type_spec.rb b/spec/graphql/types/packages/composer/metadatum_type_spec.rb
index a950c10a41d..272d518cdd5 100644
--- a/spec/graphql/types/packages/composer/metadatum_type_spec.rb
+++ b/spec/graphql/types/packages/composer/metadatum_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['ComposerMetadata'] do
it 'includes composer metadatum fields' do
expected_fields = %w[
- target_sha composer_json
+ target_sha composer_json
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_type_enum_spec.rb b/spec/graphql/types/packages/package_type_enum_spec.rb
index 9d5a7716a61..fb93b1c8c8a 100644
--- a/spec/graphql/types/packages/package_type_enum_spec.rb
+++ b/spec/graphql/types/packages/package_type_enum_spec.rb
@@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageTypeEnum'] do
it 'exposes all package types' do
- expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE])
+ expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE RPM])
end
end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 5ff7653ce39..617cbdb07fe 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Project'] do
include GraphqlHelpers
- include Ci::TemplateHelpers
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
@@ -37,7 +36,7 @@ RSpec.describe GitlabSchema.types['Project'] do
cluster_agent cluster_agents agent_configurations
ci_template timelogs merge_commit_template squash_commit_template work_item_types
recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
- timelog_categories fork_targets
+ timelog_categories fork_targets branch_rules ci_config_variables
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -509,6 +508,20 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::Ci::JobTokenScopeResolver) }
end
+ describe 'branch_rules field' do
+ subject { described_class.fields['branchRules'] }
+
+ let(:br_resolver) { Resolvers::Projects::BranchRulesResolver }
+
+ specify do
+ is_expected.to have_graphql_type(
+ Types::Projects::BranchRuleType.connection_type
+ )
+ end
+
+ specify { is_expected.to have_graphql_resolver(br_resolver) }
+ end
+
describe 'agent_configurations' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@@ -682,4 +695,54 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ describe 'branch_rules' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :public) }
+ let_it_be(:name) { 'feat/*' }
+ let_it_be(:protected_branch) do
+ create(:protected_branch, project: project, name: name)
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ branchRules {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:branch_rules_data) do
+ subject.dig('data', 'project', 'branchRules', 'nodes')
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'when a user can read protected branches' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'is present and correct' do
+ expect(branch_rules_data.count).to eq(1)
+ expect(branch_rules_data.first['name']).to eq(name)
+ end
+ end
+
+ context 'when a user cannot read protected branches' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'is empty' do
+ expect(branch_rules_data.count).to eq(0)
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
index 9b043fa52cf..860cbbf0c15 100644
--- a/spec/graphql/types/subscription_type_spec.rb
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -10,8 +10,9 @@ RSpec.describe GitlabSchema.types['Subscription'] do
issuable_title_updated
issuable_labels_updated
issuable_dates_updated
+ merge_request_reviewers_updated
]
- expect(described_class).to have_graphql_fields(*expected_fields).only
+ expect(described_class).to include_graphql_fields(*expected_fields)
end
end
diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb
index c897a25d10d..3a26ba89e04 100644
--- a/spec/graphql/types/timelog_type_spec.rb
+++ b/spec/graphql/types/timelog_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Timelog'] do
it { expect(described_class.graphql_name).to eq('Timelog') }
it { expect(described_class).to have_graphql_fields(fields) }
- it { expect(described_class).to require_graphql_authorizations(:read_issue) }
+ it { expect(described_class).to require_graphql_authorizations(:read_issuable) }
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Timelog) }
describe 'user field' do
diff --git a/spec/graphql/types/user_merge_request_interaction_type_spec.rb b/spec/graphql/types/user_merge_request_interaction_type_spec.rb
index 4782a1faf8d..3cd9750debb 100644
--- a/spec/graphql/types/user_merge_request_interaction_type_spec.rb
+++ b/spec/graphql/types/user_merge_request_interaction_type_spec.rb
@@ -76,11 +76,8 @@ RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do
context 'when the user has been asked to review the MR' do
before do
merge_request.reviewers << user
- merge_request.find_reviewer(user).update!(state: :attention_requested)
end
- it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['ATTENTION_REQUESTED'].value) }
-
it 'implies not reviewed' do
expect(resolve(:reviewed)).to be false
end
diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb
index c556424b0b4..11b02a88dbd 100644
--- a/spec/graphql/types/work_item_type_spec.rb
+++ b/spec/graphql/types/work_item_type_spec.rb
@@ -28,8 +28,6 @@ RSpec.describe GitlabSchema.types['WorkItem'] do
closed_at
]
- fields.each do |field_name|
- expect(described_class).to have_graphql_fields(*fields)
- end
+ expect(described_class).to have_graphql_fields(*fields)
end
end
diff --git a/spec/graphql/types/work_items/widgets/description_type_spec.rb b/spec/graphql/types/work_items/widgets/description_type_spec.rb
index 5ade1fe4aa2..aee388ce82a 100644
--- a/spec/graphql/types/work_items/widgets/description_type_spec.rb
+++ b/spec/graphql/types/work_items/widgets/description_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::DescriptionType do
it 'exposes the expected fields' do
- expected_fields = %i[description description_html type]
+ expected_fields = %i[description description_html edited last_edited_at last_edited_by type]
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 0304aac18ae..1703727db21 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -46,6 +46,10 @@ RSpec.describe ApplicationSettingsHelper do
expect(helper.visible_attributes).to include(:deactivate_dormant_users)
end
+ it 'contains :deactivate_dormant_users_period' do
+ expect(helper.visible_attributes).to include(:deactivate_dormant_users_period)
+ end
+
it 'contains rate limit parameters' do
expect(helper.visible_attributes).to include(*%i(
issues_create_limit notes_create_limit project_export_limit
@@ -63,6 +67,10 @@ RSpec.describe ApplicationSettingsHelper do
it 'does not contain :deactivate_dormant_users' do
expect(helper.visible_attributes).not_to include(:deactivate_dormant_users)
end
+
+ it 'does not contain :deactivate_dormant_users_period' do
+ expect(helper.visible_attributes).not_to include(:deactivate_dormant_users_period)
+ end
end
end
@@ -289,6 +297,66 @@ RSpec.describe ApplicationSettingsHelper do
end
end
+ describe '.spam_check_endpoint_enabled?' do
+ subject { helper.spam_check_endpoint_enabled? }
+
+ context 'when spam check endpoint is enabled' do
+ before do
+ stub_application_setting(spam_check_endpoint_enabled: true)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when spam check endpoint is disabled' do
+ before do
+ stub_application_setting(spam_check_endpoint_enabled: false)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '.anti_spam_service_enabled?' do
+ subject { helper.anti_spam_service_enabled? }
+
+ context 'when akismet is enabled and spam check endpoint is disabled' do
+ before do
+ stub_application_setting(spam_check_endpoint_enabled: false)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when akismet is disabled and spam check endpoint is enabled' do
+ before do
+ stub_application_setting(spam_check_endpoint_enabled: true)
+ stub_application_setting(akismet_enabled: false)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when akismet and spam check endpoint are both enabled' do
+ before do
+ stub_application_setting(spam_check_endpoint_enabled: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when akismet and spam check endpoint are both disabled' do
+ before do
+ stub_application_setting(spam_check_endpoint_enabled: false)
+ stub_application_setting(akismet_enabled: false)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
describe '#sidekiq_job_limiter_modes_for_select' do
subject { helper.sidekiq_job_limiter_modes_for_select }
@@ -305,7 +373,7 @@ RSpec.describe ApplicationSettingsHelper do
allow(helper).to receive(:can?).with(user, :read_cluster, instance_of(Clusters::Instance)).and_return(true)
end
- it { is_expected.to be_truthy}
+ it { is_expected.to be_truthy }
context ':certificate_based_clusters feature flag is disabled' do
before do
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 65e46b61882..fe652e905cc 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -357,7 +357,7 @@ RSpec.describe BlobHelper do
describe '#ide_merge_request_path' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:merge_request) { create(:merge_request, source_project: project)}
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
it 'returns IDE path for the given MR if MR is not merged' do
expect(helper.ide_merge_request_path(merge_request)).to eq("/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.iid}")
diff --git a/spec/helpers/ci/builds_helper_spec.rb b/spec/helpers/ci/builds_helper_spec.rb
index ea3b5aac4ea..c215d7b4a78 100644
--- a/spec/helpers/ci/builds_helper_spec.rb
+++ b/spec/helpers/ci/builds_helper_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe Ci::BuildsHelper do
expect(subject).to eq({
page_path: project_job_path(project, ci_build),
build_status: ci_build.status,
- build_stage: ci_build.stage,
+ build_stage: ci_build.stage_name,
log_state: ''
})
end
@@ -106,7 +106,7 @@ RSpec.describe Ci::BuildsHelper do
expect(subject).to eq([{
id: failed_build.id,
failure: failed_build.present.callout_failure_message,
- failure_summary: helper.build_summary(failed_build)
+ failure_summary: helper.build_summary(failed_build)
}].to_json)
end
end
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index 3b18572ad64..1b1edde8faf 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -131,17 +131,32 @@ RSpec.describe Ci::RunnersHelper do
describe '#group_runners_data_attributes' do
let(:group) { create(:group) }
- it 'returns group data to render a runner list' do
- expect(helper.group_runners_data_attributes(group)).to include(
- registration_token: group.runners_token,
- group_id: group.id,
- group_full_path: group.full_path,
- runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
- online_contact_timeout_secs: 7200,
- stale_timeout_secs: 7889238,
- empty_state_svg_path: start_with('/assets/illustrations/pipelines_empty'),
- empty_state_filtered_svg_path: start_with('/assets/illustrations/magnifying-glass')
- )
+ context 'when user can register group runners' do
+ before do
+ allow(helper).to receive(:can?).with(user, :register_group_runners, group).and_return(true)
+ end
+
+ it 'returns group data to render a runner list' do
+ expect(helper.group_runners_data_attributes(group)).to include(
+ group_id: group.id,
+ group_full_path: group.full_path,
+ runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
+ online_contact_timeout_secs: 7200,
+ stale_timeout_secs: 7889238,
+ empty_state_svg_path: start_with('/assets/illustrations/pipelines_empty'),
+ empty_state_filtered_svg_path: start_with('/assets/illustrations/magnifying-glass')
+ )
+ end
+ end
+
+ context 'when user cannot register group runners' do
+ before do
+ allow(helper).to receive(:can?).with(user, :register_group_runners, group).and_return(false)
+ end
+
+ it 'returns empty registration token' do
+ expect(helper.group_runners_data_attributes(group)).not_to include(registration_token: group.runners_token)
+ end
end
end
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 010100769d4..0cc53da98b2 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -329,7 +329,7 @@ RSpec.describe CommitsHelper do
it { is_expected.to include(commit.author) }
it { is_expected.to include(ref) }
- it do
+ specify do
is_expected.to include(
{
merge_request: merge_request.cache_key,
diff --git a/spec/helpers/gitlab_script_tag_helper_spec.rb b/spec/helpers/gitlab_script_tag_helper_spec.rb
index 9d71e25286e..cfe7b349cec 100644
--- a/spec/helpers/gitlab_script_tag_helper_spec.rb
+++ b/spec/helpers/gitlab_script_tag_helper_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe GitlabScriptTagHelper do
end
describe 'inline script tag' do
- let(:tag_with_nonce) {"<script nonce=\"noncevalue\">\n//<![CDATA[\nalert(1)\n//]]>\n</script>"}
- let(:tag_with_nonce_and_type) {"<script type=\"application/javascript\" nonce=\"noncevalue\">\n//<![CDATA[\nalert(1)\n//]]>\n</script>"}
+ let(:tag_with_nonce) { "<script nonce=\"noncevalue\">\n//<![CDATA[\nalert(1)\n//]]>\n</script>" }
+ let(:tag_with_nonce_and_type) { "<script type=\"application/javascript\" nonce=\"noncevalue\">\n//<![CDATA[\nalert(1)\n//]]>\n</script>" }
it 'returns a script tag with a nonce using block syntax' do
expect(helper.javascript_tag { 'alert(1)' }.to_s).to eq tag_with_nonce
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 2c1061d2f1b..00e620832b3 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -520,6 +520,29 @@ RSpec.describe GroupsHelper do
end
end
+ describe '#group_overview_tabs_app_data' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+
+ allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
+ allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
+ end
+
+ it 'returns expected hash' do
+ expect(helper.group_overview_tabs_app_data(group)).to match(
+ {
+ subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
+ shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
+ archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
+ current_group_visibility: group.visibility
+ }.merge(helper.group_overview_tabs_app_data(group))
+ )
+ end
+ end
+
describe "#enabled_git_access_protocol_options_for_group" do
subject { helper.enabled_git_access_protocol_options_for_group }
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 069465c2fec..18a21b59409 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -448,7 +448,7 @@ RSpec.describe IssuablesHelper do
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
end
- it { is_expected.to include({ can_merge: can_merge })}
+ it { is_expected.to include({ can_merge: can_merge }) }
end
end
end
@@ -480,7 +480,7 @@ RSpec.describe IssuablesHelper do
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
end
- it { is_expected.to include({ can_merge: can_merge })}
+ it { is_expected.to include({ can_merge: can_merge }) }
end
end
end
diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb
index 4d2fc3d9ee6..97e37023c2d 100644
--- a/spec/helpers/jira_connect_helper_spec.rb
+++ b/spec/helpers/jira_connect_helper_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe JiraConnectHelper do
describe '#jira_connect_app_data' do
+ let_it_be(:installation) { create(:jira_connect_installation) }
let_it_be(:subscription) { create(:jira_connect_subscription) }
let(:user) { create(:user) }
@@ -13,11 +14,12 @@ RSpec.describe JiraConnectHelper do
stub_application_setting(jira_connect_application_key: client_id)
end
- subject { helper.jira_connect_app_data([subscription]) }
+ subject { helper.jira_connect_app_data([subscription], installation) }
context 'user is not logged in' do
before do
allow(view).to receive(:current_user).and_return(nil)
+ allow(Gitlab).to receive_message_chain('config.gitlab.url') { 'http://test.host' }
end
it 'includes Jira Connect app attributes' do
@@ -36,14 +38,14 @@ RSpec.describe JiraConnectHelper do
end
context 'with oauth_metadata' do
- let(:oauth_metadata) { helper.jira_connect_app_data([subscription])[:oauth_metadata] }
+ let(:oauth_metadata) { helper.jira_connect_app_data([subscription], installation)[:oauth_metadata] }
subject(:parsed_oauth_metadata) { Gitlab::Json.parse(oauth_metadata).deep_symbolize_keys }
it 'assigns oauth_metadata' do
expect(parsed_oauth_metadata).to include(
oauth_authorize_url: start_with('http://test.host/oauth/authorize?'),
- oauth_token_url: 'http://test.host/oauth/token',
+ oauth_token_path: '/oauth/token',
state: %r/[a-z0-9.]{32}/,
oauth_token_payload: hash_including(
grant_type: 'authorization_code',
@@ -74,6 +76,30 @@ RSpec.describe JiraConnectHelper do
expect(oauth_metadata).to be_nil
end
end
+
+ context 'with self-managed instance' do
+ let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
+
+ it 'points urls to the self-managed instance' do
+ expect(parsed_oauth_metadata).to include(
+ oauth_authorize_url: start_with('https://gitlab.example.com/oauth/authorize?'),
+ oauth_token_path: '/oauth/token'
+ )
+ end
+
+ context 'and jira_connect_oauth_self_managed feature is disabled' do
+ before do
+ stub_feature_flags(jira_connect_oauth_self_managed: false)
+ end
+
+ it 'does not point urls to the self-managed instance' do
+ expect(parsed_oauth_metadata).not_to include(
+ oauth_authorize_url: start_with('https://gitlab.example.com/oauth/authorize?'),
+ oauth_token_path: 'https://gitlab.example.com/oauth/token'
+ )
+ end
+ end
+ end
end
it 'passes group as "skip_groups" param' do
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
index 7c9dfd6b5be..0d4f1965d92 100644
--- a/spec/helpers/learn_gitlab_helper_spec.rb
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -7,16 +7,16 @@ RSpec.describe LearnGitlabHelper do
include Devise::Test::ControllerHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME, namespace: user.namespace) }
+ let_it_be(:project) { create(:project, name: Onboarding::LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
let_it_be(:namespace) { project.namespace }
before do
- allow_next_instance_of(LearnGitlab::Project) do |learn_gitlab|
+ allow_next_instance_of(Onboarding::LearnGitlab) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
end
- OnboardingProgress.onboard(namespace)
- OnboardingProgress.register(namespace, :git_write)
+ Onboarding::Progress.onboard(namespace)
+ Onboarding::Progress.register(namespace, :git_write)
end
describe '#learn_gitlab_enabled?' do
@@ -37,8 +37,8 @@ RSpec.describe LearnGitlabHelper do
with_them do
before do
- allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
- allow_next(LearnGitlab::Project, user).to receive(:available?).and_return(learn_gitlab_available)
+ allow(Onboarding::Progress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
+ allow_next(Onboarding::LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available)
end
context 'when signed in' do
@@ -81,7 +81,7 @@ RSpec.describe LearnGitlabHelper do
it 'has all section data', :aggregate_failures do
expect(onboarding_sections_data.keys).to contain_exactly(:deploy, :plan, :workspace)
- expect(onboarding_sections_data.values.map { |section| section.keys }).to match_array([[:svg]] * 3)
+ expect(onboarding_sections_data.values.map(&:keys)).to match_array([[:svg]] * 3)
end
it 'has all project data', :aggregate_failures do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 4a3a623ce77..005fce1730f 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe MembersHelper do
describe '#remove_member_message' do
let(:requester) { create(:user) }
let(:project) { create(:project, :public) }
- let(:project_member) { build(:project_member, project: project) }
- let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
+ let(:project_member) { create(:project_member, project: project) }
+ let(:project_member_invite) { create(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) }
- let(:group_member) { build(:group_member, group: group) }
- let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
+ let(:group_member) { create(:group_member, group: group) }
+ let(:group_member_invite) { create(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.full_name} project?" }
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 45664a7e0bd..3a65131aab0 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Nav::NewDropdownHelper do
id: 'general_new_group',
title: 'New group',
href: '/groups/new',
- data: { track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown' }
+ data: { qa_selector: 'global_new_group_link', track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown' }
)
)
)
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index e4fa503b5ee..9c396d6bf25 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -27,9 +27,11 @@ RSpec.describe Nav::TopNavHelper do
let(:subject) { helper.top_nav_view_model(project: current_project, group: current_group) }
- let(:active_title) { 'Menu' }
+ let(:menu_title) { 'Menu' }
before do
+ stub_feature_flags(new_navbar_layout: false)
+
allow(Gitlab::CurrentSettings).to receive(:admin_mode) { with_current_settings_admin_mode }
allow(helper).to receive(:header_link?).with(:admin_mode) { with_header_link_admin_mode }
@@ -44,12 +46,15 @@ RSpec.describe Nav::TopNavHelper do
allow(helper).to receive(:dashboard_nav_link?).with(:activity) { with_activity }
end
- it 'has :activeTitle' do
- expect(subject[:activeTitle]).to eq(active_title)
+ it 'has :menuTitle' do
+ expect(subject[:menuTitle]).to eq(menu_title)
end
context 'when current_user is nil (anonymous)' do
it 'has expected :primary' do
+ expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
+ title: 'Explore'
+ )
expected_primary = [
{ href: '/explore', icon: 'project', id: 'project', title: 'Projects' },
{ href: '/explore/groups', icon: 'group', id: 'groups', title: 'Groups' },
@@ -58,7 +63,7 @@ RSpec.describe Nav::TopNavHelper do
::Gitlab::Nav::TopNavMenuItem.build(**item)
end
- expect(subject[:primary]).to eq(expected_primary)
+ expect(subject[:primary]).to eq([expected_header, *expected_primary])
end
it 'has expected :shortcuts' do
@@ -103,7 +108,7 @@ RSpec.describe Nav::TopNavHelper do
let(:current_user) { user }
it 'has no menu items or views by default' do
- expect(subject).to eq({ activeTitle: active_title,
+ expect(subject).to eq({ menuTitle: menu_title,
primary: [],
secondary: [],
shortcuts: [],
@@ -115,6 +120,9 @@ RSpec.describe Nav::TopNavHelper do
let(:projects_view) { subject[:views][:projects] }
it 'has expected :primary' do
+ expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
+ title: 'Switch to'
+ )
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-projects-dropdown',
data: {
@@ -126,7 +134,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Projects',
view: 'projects'
)
- expect(subject[:primary]).to eq([expected_primary])
+ expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@@ -153,61 +161,87 @@ RSpec.describe Nav::TopNavHelper do
::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'menu_item_link',
- qa_title: 'Your projects',
- **menu_data_tracking_attrs('your_projects')
+ qa_title: 'View all projects',
+ **menu_data_tracking_attrs('view_all_projects')
},
href: '/dashboard/projects',
id: 'your',
- title: 'Your projects'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Starred projects',
- **menu_data_tracking_attrs('starred_projects')
- },
- href: '/dashboard/projects/starred',
- id: 'starred',
- title: 'Starred projects'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Explore projects',
- **menu_data_tracking_attrs('explore_projects')
- },
- href: '/explore',
- id: 'explore',
- title: 'Explore projects'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Explore topics',
- **menu_data_tracking_attrs('explore_topics')
- },
- href: '/explore/projects/topics',
- id: 'topics',
- title: 'Explore topics'
+ title: 'View all projects'
)
]
expect(projects_view[:linksPrimary]).to eq(expected_links_primary)
end
- it 'has expected :linksSecondary' do
- expected_links_secondary = [
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Create new project',
- **menu_data_tracking_attrs('create_new_project')
- },
- href: '/projects/new',
- id: 'create',
- title: 'Create new project'
- )
- ]
- expect(projects_view[:linksSecondary]).to eq(expected_links_secondary)
+ it 'does not have any :linksSecondary' do
+ expect(projects_view[:linksSecondary]).to eq([])
+ end
+
+ context 'when extra submenu options are not hidden' do
+ before do
+ stub_feature_flags(remove_extra_primary_submenu_options: false)
+ end
+
+ it 'has expected :linksPrimary' do
+ expected_links_primary = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Your projects',
+ **menu_data_tracking_attrs('your_projects')
+ },
+ href: '/dashboard/projects',
+ id: 'your',
+ title: 'Your projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Starred projects',
+ **menu_data_tracking_attrs('starred_projects')
+ },
+ href: '/dashboard/projects/starred',
+ id: 'starred',
+ title: 'Starred projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Explore projects',
+ **menu_data_tracking_attrs('explore_projects')
+ },
+ href: '/explore',
+ id: 'explore',
+ title: 'Explore projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Explore topics',
+ **menu_data_tracking_attrs('explore_topics')
+ },
+ href: '/explore/projects/topics',
+ id: 'topics',
+ title: 'Explore topics'
+ )
+ ]
+ expect(projects_view[:linksPrimary]).to eq(expected_links_primary)
+ end
+
+ it 'has expected :linksSecondary' do
+ expected_links_secondary = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Create new project',
+ **menu_data_tracking_attrs('create_new_project')
+ },
+ href: '/projects/new',
+ id: 'create',
+ title: 'Create new project'
+ )
+ ]
+ expect(projects_view[:linksSecondary]).to eq(expected_links_secondary)
+ end
end
context 'with current nav as project' do
@@ -251,6 +285,9 @@ RSpec.describe Nav::TopNavHelper do
let(:groups_view) { subject[:views][:groups] }
it 'has expected :primary' do
+ expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
+ title: 'Switch to'
+ )
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-groups-dropdown',
data: {
@@ -262,7 +299,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Groups',
view: 'groups'
)
- expect(subject[:primary]).to eq([expected_primary])
+ expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@@ -289,41 +326,67 @@ RSpec.describe Nav::TopNavHelper do
::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'menu_item_link',
- qa_title: 'Your groups',
- **menu_data_tracking_attrs('your_groups')
+ qa_title: 'View all groups',
+ **menu_data_tracking_attrs('view_all_groups')
},
href: '/dashboard/groups',
id: 'your',
- title: 'Your groups'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Explore groups',
- **menu_data_tracking_attrs('explore_groups')
- },
- href: '/explore/groups',
- id: 'explore',
- title: 'Explore groups'
+ title: 'View all groups'
)
]
expect(groups_view[:linksPrimary]).to eq(expected_links_primary)
end
- it 'has expected :linksSecondary' do
- expected_links_secondary = [
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Create group',
- **menu_data_tracking_attrs('create_group')
- },
- href: '/groups/new',
- id: 'create',
- title: 'Create group'
- )
- ]
- expect(groups_view[:linksSecondary]).to eq(expected_links_secondary)
+ it 'does not have any :linksSecondary' do
+ expect(groups_view[:linksSecondary]).to eq([])
+ end
+
+ context 'when extra submenu options are not hidden' do
+ before do
+ stub_feature_flags(remove_extra_primary_submenu_options: false)
+ end
+
+ it 'has expected :linksPrimary' do
+ expected_links_primary = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Your groups',
+ **menu_data_tracking_attrs('your_groups')
+ },
+ href: '/dashboard/groups',
+ id: 'your',
+ title: 'Your groups'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Explore groups',
+ **menu_data_tracking_attrs('explore_groups')
+ },
+ href: '/explore/groups',
+ id: 'explore',
+ title: 'Explore groups'
+ )
+ ]
+ expect(groups_view[:linksPrimary]).to eq(expected_links_primary)
+ end
+
+ it 'has expected :linksSecondary' do
+ expected_links_secondary = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ data: {
+ qa_selector: 'menu_item_link',
+ qa_title: 'Create group',
+ **menu_data_tracking_attrs('create_group')
+ },
+ href: '/groups/new',
+ id: 'create',
+ title: 'Create group'
+ )
+ ]
+ expect(groups_view[:linksSecondary]).to eq(expected_links_secondary)
+ end
end
context 'with external user' do
@@ -374,6 +437,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_milestones) { true }
it 'has expected :primary' do
+ expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
+ title: 'Explore'
+ )
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'milestones_link',
@@ -384,7 +450,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'milestones',
title: 'Milestones'
)
- expect(subject[:primary]).to eq([expected_primary])
+ expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@@ -402,6 +468,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_snippets) { true }
it 'has expected :primary' do
+ expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
+ title: 'Explore'
+ )
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'snippets_link',
@@ -412,7 +481,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'snippets',
title: 'Snippets'
)
- expect(subject[:primary]).to eq([expected_primary])
+ expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@@ -430,6 +499,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_activity) { true }
it 'has expected :primary' do
+ expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
+ title: 'Explore'
+ )
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'activity_link',
@@ -440,7 +512,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'activity',
title: 'Activity'
)
- expect(subject[:primary]).to eq([expected_primary])
+ expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
diff --git a/spec/helpers/notify_helper_spec.rb b/spec/helpers/notify_helper_spec.rb
index 654fb9bb3f8..09da2b89dff 100644
--- a/spec/helpers/notify_helper_spec.rb
+++ b/spec/helpers/notify_helper_spec.rb
@@ -51,6 +51,33 @@ RSpec.describe NotifyHelper do
end
end
+ describe '#merge_request_hash_param' do
+ let(:merge_request) { create(:merge_request) }
+ let(:reviewer) { create(:user) }
+ let(:avatar_icon_for_user) { 'avatar_icon_for_user' }
+
+ before do
+ allow(helper).to receive(:avatar_icon_for_user).and_return(avatar_icon_for_user)
+ end
+
+ it 'returns MR approved description' do
+ mr_link_style = "font-weight: 600;color:#3777b0;text-decoration:none"
+ reviewer_avatar_style = "border-radius:12px;margin:-7px 0 -7px 3px;"
+ mr_link = link_to(merge_request.to_reference, merge_request_url(merge_request), style: mr_link_style).html_safe
+ reviewer_avatar = content_tag(:img, nil, height: "24", src: avatar_icon_for_user, style: reviewer_avatar_style, \
+ width: "24", alt: "Avatar", class: "avatar").html_safe
+ reviewer_link = link_to(reviewer.name, user_url(reviewer), style: "color:#333333;text-decoration:none;", \
+ class: "muted").html_safe
+ result = helper.merge_request_hash_param(merge_request, reviewer)
+ expect(result[:mr_highlight]).to eq '<span style="font-weight: 600;color:#333333;">'.html_safe
+ expect(result[:highlight_end]).to eq '</span>'.html_safe
+ expect(result[:mr_link]).to eq mr_link
+ expect(result[:reviewer_highlight]).to eq '<span>'.html_safe
+ expect(result[:reviewer_avatar]).to eq reviewer_avatar
+ expect(result[:reviewer_link]).to eq reviewer_link
+ end
+ end
+
def reference_link(entity, url)
"<a href=\"#{url}\">#{entity.to_reference}</a>"
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index d0d399ad10f..1e16d969744 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -222,6 +222,22 @@ RSpec.describe PageLayoutHelper do
end
end
+ describe '#full_content_class' do
+ before do
+ allow(helper).to receive(:current_user).and_return(build(:user))
+ end
+
+ it 'has a content_class set' do
+ assign(:content_class, '_content_class_')
+
+ expect(helper.full_content_class).to eq 'container-fluid container-limited _content_class_'
+ end
+
+ it 'has no content_class set' do
+ expect(helper.full_content_class).to eq 'container-fluid container-limited '
+ end
+ end
+
describe '#user_status_properties' do
let(:user) { build(:user) }
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index 63641e65942..7de8ca89d3d 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe ProfilesHelper do
describe "#ssh_key_expires_field_description" do
subject { helper.ssh_key_expires_field_description }
- it { is_expected.to eq('Key becomes invalid on this date.') }
+ it { is_expected.to eq(s_('Profiles|Optional but recommended. If set, key becomes invalid on the specified date.')) }
end
describe '#middle_dot_divider_classes' do
diff --git a/spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb b/spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb
new file mode 100644
index 00000000000..6b82518592f
--- /dev/null
+++ b/spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::GoogleCloud::CloudsqlHelper do
+ describe '#TIERS' do
+ it 'is an array' do
+ expect(described_class::TIERS).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#VERSIONS' do
+ it 'returns versions for :postgres' do
+ expect(described_class::VERSIONS[:postgres]).to be_an_instance_of(Array)
+ end
+
+ it 'returns versions for :mysql' do
+ expect(described_class::VERSIONS[:mysql]).to be_an_instance_of(Array)
+ end
+
+ it 'returns versions for :sqlserver' do
+ expect(described_class::VERSIONS[:sqlserver]).to be_an_instance_of(Array)
+ end
+ end
+end
diff --git a/spec/helpers/projects/pages_helper_spec.rb b/spec/helpers/projects/pages_helper_spec.rb
new file mode 100644
index 00000000000..4a4cebc9d70
--- /dev/null
+++ b/spec/helpers/projects/pages_helper_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::PagesHelper do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ stub_config(pages: {
+ access_control: true,
+ external_http: true,
+ external_https: true,
+ host: "new.domain.com"
+ })
+ end
+
+ context 'when the user have permission' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'on custom domain' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:external_http, :external_https, :can_create) do
+ false | false | false
+ false | true | true
+ true | false | true
+ true | true | true
+ end
+
+ with_them do
+ it do
+ stub_config(pages: { external_http: external_http, external_https: external_https })
+
+ expect(can_create_pages_custom_domains?(user, project)).to be can_create
+ end
+ end
+ end
+
+ context 'on domain limit' do
+ it 'can create new domains when the limit is 0' do
+ Gitlab::CurrentSettings.update!(max_pages_custom_domains_per_project: 0)
+
+ expect(can_create_pages_custom_domains?(user, project)).to be true
+ end
+
+ it 'validates custom domain creation is only allowed upto max value' do
+ Gitlab::CurrentSettings.update!(max_pages_custom_domains_per_project: 1)
+
+ expect(can_create_pages_custom_domains?(user, project)).to be true
+ create(:pages_domain, project: project)
+ expect(can_create_pages_custom_domains?(user, project)).to be false
+ end
+ end
+ end
+
+ context 'when the user does not have permission' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'validates user cannot create domain' do
+ expect(can_create_pages_custom_domains?(user, project)).to be false
+ end
+ end
+end
diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb
index 8ce4e9f5293..a70544ace1a 100644
--- a/spec/helpers/projects/pipeline_helper_spec.rb
+++ b/spec/helpers/projects/pipeline_helper_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Projects::PipelineHelper do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:raw_pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
- let_it_be(:pipeline) { Ci::PipelinePresenter.new(raw_pipeline, current_user: user)}
+ let_it_be(:pipeline) { Ci::PipelinePresenter.new(raw_pipeline, current_user: user) }
describe '#js_pipeline_tabs_data' do
before do
@@ -19,7 +19,6 @@ RSpec.describe Projects::PipelineHelper do
it 'returns pipeline tabs data' do
expect(pipeline_tabs_data).to include({
- can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
failed_jobs_count: pipeline.failed_builds.count,
failed_jobs_summary: prepare_failed_jobs_summary_data(pipeline.failed_builds),
full_path: project.full_path,
@@ -31,9 +30,10 @@ RSpec.describe Projects::PipelineHelper do
summary_endpoint: summary_project_pipeline_tests_path(project, pipeline, format: :json),
suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(project, pipeline.sha),
- has_test_report: pipeline.has_reports?(Ci::JobArtifact.test_reports),
+ has_test_report: pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)),
empty_state_image_path: match_asset_path('illustrations/empty-state/empty-test-cases-lg.svg'),
- artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg')
+ artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'),
+ tests_count: pipeline.test_report_summary.total[:count]
})
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 04c066986b7..a9db2a1c008 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -1147,37 +1147,23 @@ RSpec.describe ProjectsHelper do
context 'with the setting enabled' do
before do
stub_application_setting(delete_inactive_projects: true)
+ stub_application_setting(inactive_projects_min_size_mb: 0)
+ stub_application_setting(inactive_projects_send_warning_email_after_months: 1)
end
- context 'with the feature flag disabled' do
- before do
- stub_feature_flags(inactive_projects_deletion: false)
- end
-
+ context 'with an active project' do
it_behaves_like 'does not show the banner'
end
- context 'with the feature flag enabled' do
+ context 'with an inactive project' do
before do
- stub_feature_flags(inactive_projects_deletion: true)
- stub_application_setting(inactive_projects_min_size_mb: 0)
- stub_application_setting(inactive_projects_send_warning_email_after_months: 1)
+ project.statistics.storage_size = 1.megabyte
+ project.last_activity_at = 1.year.ago
+ project.save!
end
- context 'with an active project' do
- it_behaves_like 'does not show the banner'
- end
-
- context 'with an inactive project' do
- before do
- project.statistics.storage_size = 1.megabyte
- project.last_activity_at = 1.year.ago
- project.save!
- end
-
- it 'shows the banner' do
- expect(helper.show_inactive_project_deletion_banner?(project)).to be(true)
- end
+ it 'shows the banner' do
+ expect(helper.show_inactive_project_deletion_banner?(project)).to be(true)
end
end
end
@@ -1304,7 +1290,7 @@ RSpec.describe ProjectsHelper do
let_it_be(:has_active_license) { true }
it 'displays the correct messagee' do
- expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support.'))
+ expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of February 2023. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support.'))
end
end
@@ -1312,7 +1298,7 @@ RSpec.describe ProjectsHelper do
let_it_be(:has_active_license) { false }
it 'displays the correct message' do
- expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.'))
+ expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of February 2023. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.'))
end
end
end
diff --git a/spec/helpers/routing/pseudonymization_helper_spec.rb b/spec/helpers/routing/pseudonymization_helper_spec.rb
index dd4cc55ed2b..eb2cb548f35 100644
--- a/spec/helpers/routing/pseudonymization_helper_spec.rb
+++ b/spec/helpers/routing/pseudonymization_helper_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do
end
context 'with controller for groups with subgroups and project' do
- let(:masked_url) { "http://localhost/namespace#{subgroup.id}/project#{subproject.id}"}
+ let(:masked_url) { "http://localhost/namespace#{subgroup.id}/project#{subproject.id}" }
let(:group) { subgroup }
let(:project) { subproject }
let(:request) do
@@ -94,7 +94,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do
end
context 'with controller for groups and subgroups' do
- let(:masked_url) { "http://localhost/groups/namespace#{subgroup.id}/-/shared"}
+ let(:masked_url) { "http://localhost/groups/namespace#{subgroup.id}/-/shared" }
let(:group) { subgroup }
let(:request) do
double(:Request,
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 513e2865ee3..ad0705e4fbf 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -625,7 +625,7 @@ RSpec.describe SearchHelper do
false | false
end
- let(:params) {{ confidential: confidential }}
+ let(:params) { { confidential: confidential } }
with_them do
it 'transforms confidentiality param' do
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index 1ee920d1c95..3e555301325 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -74,11 +74,11 @@ RSpec.describe SortingHelper do
def project_common_options
{
- sort_value_latest_activity => sort_title_latest_activity,
+ sort_value_latest_activity => sort_title_latest_activity,
sort_value_recently_created => sort_title_created_date,
- sort_value_name => sort_title_name,
- sort_value_name_desc => sort_title_name_desc,
- sort_value_stars_desc => sort_title_stars
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_stars_desc => sort_title_stars
}
end
@@ -90,11 +90,11 @@ RSpec.describe SortingHelper do
describe '#projects_sort_options_hash' do
it 'returns a hash of available sorting options' do
admin_options = project_common_options.merge({
- sort_value_oldest_activity => sort_title_oldest_activity,
- sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_activity => sort_title_oldest_activity,
+ sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_created => sort_title_recently_created,
- sort_value_stars_desc => sort_title_most_stars,
- sort_value_largest_repo => sort_title_largest_repo
+ sort_value_stars_desc => sort_title_most_stars,
+ sort_value_largest_repo => sort_title_largest_repo
})
expect(projects_sort_options_hash).to eq(admin_options)
@@ -180,10 +180,10 @@ RSpec.describe SortingHelper do
describe '#projects_sort_option_titles' do
it 'returns a hash of titles for the sorting options' do
options = project_common_options.merge({
- sort_value_oldest_activity => sort_title_latest_activity,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_name_desc => sort_title_name,
- sort_value_stars_asc => sort_title_stars
+ sort_value_oldest_activity => sort_title_latest_activity,
+ sort_value_oldest_created => sort_title_created_date,
+ sort_value_name_desc => sort_title_name,
+ sort_value_stars_asc => sort_title_stars
})
expect(projects_sort_option_titles).to eq(options)
@@ -198,10 +198,10 @@ RSpec.describe SortingHelper do
describe '#projects_sort_options_hash' do
it 'returns a hash of available sorting options' do
options = project_common_options.merge({
- sort_value_oldest_activity => sort_title_oldest_activity,
- sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_activity => sort_title_oldest_activity,
+ sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_created => sort_title_recently_created,
- sort_value_stars_desc => sort_title_most_stars
+ sort_value_stars_desc => sort_title_most_stars
})
expect(projects_sort_options_hash).to eq(options)
@@ -210,6 +210,18 @@ RSpec.describe SortingHelper do
end
end
+ describe '#tags_sort_options_hash' do
+ it 'returns a hash of available sorting options' do
+ expect(tags_sort_options_hash).to include({
+ sort_value_name => sort_title_name,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_version_desc => sort_title_version_desc,
+ sort_value_version_asc => sort_title_version_asc
+ })
+ end
+ end
+
describe 'with `forks` controller' do
before do
stub_controller_path 'forks'
@@ -219,9 +231,9 @@ RSpec.describe SortingHelper do
it 'returns a hash of available sorting options' do
expect(forks_sort_options_hash).to include({
sort_value_recently_created => sort_title_created_date,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_latest_activity => sort_title_latest_activity,
- sort_value_oldest_activity => sort_title_latest_activity
+ sort_value_oldest_created => sort_title_created_date,
+ sort_value_latest_activity => sort_title_latest_activity,
+ sort_value_oldest_activity => sort_title_latest_activity
})
end
end
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 6c3556c874b..6c0f1034d65 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -27,15 +27,15 @@ RSpec.describe StorageHelper do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 10.kilobytes,
- wiki_size: 10.bytes,
- lfs_objects_size: 20.gigabytes,
- build_artifacts_size: 30.megabytes,
+ namespace: namespace,
+ repository_size: 10.kilobytes,
+ wiki_size: 10.bytes,
+ lfs_objects_size: 20.gigabytes,
+ build_artifacts_size: 30.megabytes,
pipeline_artifacts_size: 11.megabytes,
- snippets_size: 40.megabytes,
- packages_size: 12.megabytes,
- uploads_size: 15.megabytes))
+ snippets_size: 40.megabytes,
+ packages_size: 12.megabytes,
+ uploads_size: 15.megabytes))
end
let(:message) { 'Repository: 10 KB / Wikis: 10 Bytes / Build Artifacts: 30 MB / Pipeline Artifacts: 11 MB / LFS: 20 GB / Snippets: 40 MB / Packages: 12 MB / Uploads: 15 MB' }
@@ -50,147 +50,4 @@ RSpec.describe StorageHelper do
expect(helper.storage_counters_details(namespace_stats)).to eq(message)
end
end
-
- describe "storage_enforcement_banner" do
- let_it_be_with_refind(:current_user) { create(:user) }
- let_it_be(:free_group) { create(:group) }
- let_it_be(:paid_group) { create(:group) }
-
- before do
- allow(helper).to receive(:can?).with(current_user, :maintainer_access, free_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :maintainer_access, paid_group).and_return(true)
- allow(helper).to receive(:current_user) { current_user }
- allow(paid_group).to receive(:paid?).and_return(true)
-
- stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
- end
-
- describe "#storage_enforcement_banner_info" do
- it 'returns nil when namespace is not free' do
- expect(helper.storage_enforcement_banner_info(paid_group)).to be(nil)
- end
-
- it 'returns nil when storage_enforcement_date is not set' do
- allow(free_group).to receive(:storage_enforcement_date).and_return(nil)
-
- expect(helper.storage_enforcement_banner_info(free_group)).to be(nil)
- end
-
- describe 'when storage_enforcement_date is set' do
- let_it_be(:storage_enforcement_date) { Date.today + 30 }
-
- before do
- allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
-
- it 'returns nil when current_user do not have access usage quotas page' do
- allow(helper).to receive(:can?).with(current_user, :maintainer_access, free_group).and_return(false)
-
- expect(helper.storage_enforcement_banner_info(free_group)).to be(nil)
- end
-
- it 'returns nil when namespace_storage_limit_show_preenforcement_banner FF is disabled' do
- stub_feature_flags(namespace_storage_limit_show_preenforcement_banner: false)
-
- expect(helper.storage_enforcement_banner_info(free_group)).to be(nil)
- end
-
- context 'when current_user can access the usage quotas page' do
- it 'returns a hash' do
- used_storage = helper.storage_counter(free_group.root_storage_statistics&.storage_size || 0)
-
- expect(helper.storage_enforcement_banner_info(free_group)).to eql({
- text_paragraph_1: "Effective #{storage_enforcement_date}, namespace storage limits will apply to the <strong>#{free_group.name}</strong> namespace. View the <a href=\"/help/user/usage_quotas#namespace-storage-limit-enforcement-schedule\" >rollout schedule for this change</a>.",
- text_paragraph_2: "The namespace is currently using <strong>#{used_storage}</strong> of namespace storage. Group owners can view namespace storage usage and purchase more from <strong>Group settings &gt; Usage quotas</strong>. <a href=\"/help/user/usage_quotas#manage-your-storage-usage\" >Learn more.</a>",
- text_paragraph_3: "See our <a href=\"https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier\" >FAQ</a> for more information.",
- variant: 'warning',
- namespace_id: free_group.id,
- callouts_feature_name: 'storage_enforcement_banner_second_enforcement_threshold',
- callouts_path: '/-/users/group_callouts'
- })
- end
-
- context 'when namespace has used storage' do
- before do
- create(:namespace_root_storage_statistics, namespace: free_group, storage_size: 102400)
- end
-
- it 'returns a hash with the correct storage size text' do
- expect(helper.storage_enforcement_banner_info(free_group)[:text_paragraph_2]).to eql("The namespace is currently using <strong>100 KB</strong> of namespace storage. Group owners can view namespace storage usage and purchase more from <strong>Group settings &gt; Usage quotas</strong>. <a href=\"/help/user/usage_quotas#manage-your-storage-usage\" >Learn more.</a>")
- end
- end
-
- context 'when the given group is a sub-group' do
- let_it_be(:sub_group) { build(:group) }
-
- before do
- allow(helper).to receive(:can?).with(current_user, :maintainer_access, sub_group).and_return(true)
- allow(sub_group).to receive(:root_ancestor).and_return(free_group)
- end
-
- it 'returns the banner hash' do
- expect(helper.storage_enforcement_banner_info(sub_group).keys).to match_array(%i(text_paragraph_1 text_paragraph_2 text_paragraph_3 variant namespace_id callouts_feature_name callouts_path))
- end
- end
- end
- end
-
- context 'when the :storage_banner_bypass_date_check is enabled', :freeze_time do
- before do
- stub_feature_flags(namespace_storage_limit_bypass_date_check: true)
- end
-
- it 'returns the enforcement info' do
- puts helper.storage_enforcement_banner_info(free_group)[:text_paragraph_1]
- expect(helper.storage_enforcement_banner_info(free_group)[:text_paragraph_1]).to include("Effective #{Date.current}, namespace storage limits will apply")
- end
- end
-
- context 'when storage_enforcement_date is set and dismissed callout exists' do
- before do
- create(:group_callout,
- user: current_user,
- group_id: free_group.id,
- feature_name: 'storage_enforcement_banner_second_enforcement_threshold')
- storage_enforcement_date = Date.today + 30
- allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- end
-
- it { expect(helper.storage_enforcement_banner_info(free_group)).to be(nil) }
- end
-
- context 'callouts_feature_name' do
- let(:days_from_now) { 45 }
-
- subject do
- storage_enforcement_date = Date.today + days_from_now
- allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
-
- helper.storage_enforcement_banner_info(free_group)[:callouts_feature_name]
- end
-
- it 'returns first callouts_feature_name' do
- is_expected.to eq('storage_enforcement_banner_first_enforcement_threshold')
- end
-
- context 'returns second callouts_feature_name' do
- let(:days_from_now) { 20 }
-
- it { is_expected.to eq('storage_enforcement_banner_second_enforcement_threshold') }
- end
-
- context 'returns third callouts_feature_name' do
- let(:days_from_now) { 13 }
-
- it { is_expected.to eq('storage_enforcement_banner_third_enforcement_threshold') }
- end
-
- context 'returns fourth callouts_feature_name' do
- let(:days_from_now) { 3 }
-
- it { is_expected.to eq('storage_enforcement_banner_fourth_enforcement_threshold') }
- end
- end
- end
- end
end
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
index dd5707e2aff..80a1224abbb 100644
--- a/spec/helpers/tab_helper_spec.rb
+++ b/spec/helpers/tab_helper_spec.rb
@@ -182,7 +182,7 @@ RSpec.describe TabHelper do
context 'with data attributes' do
it 'creates a tab counter badge with the data attributes' do
expect(helper.gl_tab_counter_badge(1, { data: { some_attribute: 'foo' } })).to eq(
- '<span data-some-attribute="foo" class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge">1</span>'
+ '<span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge" data-some-attribute="foo">1</span>'
)
end
end
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index bbabfedc3ee..a8945424877 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -258,6 +258,21 @@ RSpec.describe TodosHelper do
end
end
+ describe '#no_todos_messages' do
+ context 'when getting todos messsages' do
+ it 'return these sentences' do
+ expected_sentences = [
+ s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'),
+ s_('Todos|Isn\'t an empty To-Do List beautiful?'),
+ s_('Todos|Give yourself a pat on the back!'),
+ s_('Todos|Nothing left to do. High five!'),
+ s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"')
+ ]
+ expect(helper.no_todos_messages).to eq(expected_sentences)
+ end
+ end
+ end
+
describe '#todo_author_display?' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index 2c148aabead..170ae098a2f 100644
--- a/spec/helpers/users/callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -241,10 +241,10 @@ RSpec.describe Users::CalloutsHelper do
context 'the web-hook failure callout has been dismissed', :freeze_time do
before do
- create(:namespace_callout,
+ create(:project_callout,
feature_name: described_class::WEB_HOOK_DISABLED,
user: user,
- namespace: project.namespace,
+ project: project,
dismissed_at: 1.week.ago)
end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 78a15f52be5..617a796781e 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(blocked_user)
- expect(filter_ee_badges(badges)).to match_array([text: "Blocked", variant: "danger"])
+ expect(filter_ee_badges(badges)).to match_array([text: s_("AdminUsers|Blocked"), variant: "danger"])
end
end
@@ -146,7 +146,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(blocked_pending_approval_user)
- expect(filter_ee_badges(badges)).to match_array([text: 'Pending approval', variant: 'info'])
+ expect(filter_ee_badges(badges)).to match_array([text: s_('AdminUsers|Pending approval'), variant: 'info'])
end
end
@@ -156,7 +156,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(banned_user)
- expect(filter_ee_badges(badges)).to match_array([text: 'Banned', variant: 'danger'])
+ expect(filter_ee_badges(badges)).to match_array([text: s_('AdminUsers|Banned'), variant: 'danger'])
end
end
@@ -166,7 +166,17 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(admin_user)
- expect(filter_ee_badges(badges)).to match_array([text: "Admin", variant: "success"])
+ expect(filter_ee_badges(badges)).to match_array([text: s_("AdminUsers|Admin"), variant: "success"])
+ end
+ end
+
+ context 'with a bot' do
+ it "returns the bot badge" do
+ bot = create(:user, :bot)
+
+ badges = helper.user_badges_in_admin_section(bot)
+
+ expect(filter_ee_badges(badges)).to match_array([text: s_('AdminUsers|Bot'), variant: "muted"])
end
end
@@ -176,7 +186,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(external_user)
- expect(filter_ee_badges(badges)).to match_array([text: "External", variant: "secondary"])
+ expect(filter_ee_badges(badges)).to match_array([text: s_("AdminUsers|External"), variant: "secondary"])
end
end
@@ -184,7 +194,7 @@ RSpec.describe UsersHelper do
it 'returns the "It\'s You" badge' do
badges = helper.user_badges_in_admin_section(user)
- expect(filter_ee_badges(badges)).to match_array([text: "It's you!", variant: "muted"])
+ expect(filter_ee_badges(badges)).to match_array([text: s_("AdminUsers|It's you!"), variant: "muted"])
end
end
@@ -195,9 +205,9 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(user)
expect(badges).to match_array([
- { text: "Blocked", variant: "danger" },
- { text: "Admin", variant: "success" },
- { text: "External", variant: "secondary" }
+ { text: s_("AdminUsers|Blocked"), variant: "danger" },
+ { text: s_("AdminUsers|Admin"), variant: "success" },
+ { text: s_("AdminUsers|External"), variant: "secondary" }
])
end
end
@@ -208,7 +218,7 @@ RSpec.describe UsersHelper do
badges = helper.user_badges_in_admin_section(locked_user)
- expect(filter_ee_badges(badges)).to match_array([text: "Locked", variant: "warning"])
+ expect(filter_ee_badges(badges)).to match_array([text: s_("AdminUsers|Locked"), variant: "warning"])
end
end
@@ -435,7 +445,7 @@ RSpec.describe UsersHelper do
it 'contains resend confirmation e-mail text' do
expect(user_email_help_text).to include _('Resend confirmation e-mail')
- expect(user_email_help_text).to include _('Please click the link in the confirmation email before continuing. It was sent to ')
+ expect(user_email_help_text).to match /Please click the link in the confirmation email before continuing. It was sent to.*#{user.unconfirmed_email}/
end
end
end
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index 5adcbe3334d..75128d758f9 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -132,11 +132,11 @@ RSpec.describe WikiHelper do
it 'returns the tracking context' do
expect(subject).to eq(
- 'wiki-format' => :markdown,
- 'wiki-title-size' => 9,
- 'wiki-content-size' => 4,
+ 'wiki-format' => :markdown,
+ 'wiki-title-size' => 9,
+ 'wiki-content-size' => 4,
'wiki-directory-nest-level' => 2,
- 'wiki-container-type' => 'Project'
+ 'wiki-container-type' => 'Project'
)
end
diff --git a/spec/helpers/wiki_page_version_helper_spec.rb b/spec/helpers/wiki_page_version_helper_spec.rb
index bc500c28c5a..a792e5df035 100644
--- a/spec/helpers/wiki_page_version_helper_spec.rb
+++ b/spec/helpers/wiki_page_version_helper_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe WikiPageVersionHelper do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:user) { create(:user, username: 'foo') }
- let(:commit_with_user) { create(:commit, project: project, author: user)}
- let(:commit_without_user) { create(:commit, project: project, author_name: 'Foo', author_email: 'foo@example.com')}
+ let(:commit_with_user) { create(:commit, project: project, author: user) }
+ let(:commit_without_user) { create(:commit, project: project, author_name: 'Foo', author_email: 'foo@example.com') }
let(:wiki_page_version) { Gitlab::Git::WikiPageVersion.new(commit, nil) }
describe '#wiki_page_version_author_url' do
diff --git a/spec/initializers/00_rails_disable_joins_spec.rb b/spec/initializers/00_rails_disable_joins_spec.rb
index 78e78b6810b..3b390f1ef17 100644
--- a/spec/initializers/00_rails_disable_joins_spec.rb
+++ b/spec/initializers/00_rails_disable_joins_spec.rb
@@ -98,8 +98,8 @@ RSpec.describe 'DisableJoins' do
primary_model.has_one :test_bridge, anonymous_class: bridge_model, foreign_key: :primary_record_id
bridge_model.belongs_to :test_secondary, anonymous_class: secondary_model, foreign_key: :secondary_record_id
- primary_model.has_one :test_secondary, through: :test_bridge, anonymous_class: secondary_model,
- disable_joins: -> { joins_disabled_flag }
+ primary_model.has_one :test_secondary,
+ through: :test_bridge, anonymous_class: secondary_model, disable_joins: -> { joins_disabled_flag }
primary_record = primary_model.create!
secondary_record = secondary_model.create!
@@ -149,7 +149,7 @@ RSpec.describe 'DisableJoins' do
primary_model.has_many :test_bridges, anonymous_class: bridge_model, foreign_key: :primary_record_id
bridge_model.has_many :test_secondaries, anonymous_class: secondary_model, foreign_key: :bridge_record_id
primary_model.has_many :test_secondaries, through: :test_bridges, anonymous_class: secondary_model,
- disable_joins: -> { disabled_join_flag }
+ disable_joins: -> { disabled_join_flag }
primary_record = primary_model.create!
bridge_record = bridge_model.create!(primary_record_id: primary_record.id)
diff --git a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
index 074df9adc21..94134ce44fd 100644
--- a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
+++ b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do
sub = ActionCable.server.pubsub.send(:redis_connection)
- expect(sub.connection[:id]).to eq('redis:///home/localuser/redis/redis.socket/0')
+ expect(sub.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
expect(ActionCable.server.config.cable[:id]).to be_nil
end
end
diff --git a/spec/initializers/carrierwave_patch_spec.rb b/spec/initializers/carrierwave_patch_spec.rb
index b0f337935ef..0910342f10f 100644
--- a/spec/initializers/carrierwave_patch_spec.rb
+++ b/spec/initializers/carrierwave_patch_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'CarrierWave::Storage::Fog::File' do
let(:storage) { CarrierWave::Storage::Fog.new(uploader) }
let(:bucket_name) { 'some-bucket' }
let(:connection) { ::Fog::Storage.new(connection_options) }
- let(:bucket) { connection.directories.new(key: bucket_name )}
+ let(:bucket) { connection.directories.new(key: bucket_name ) }
let(:test_filename) { 'test' }
let(:test_data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
@@ -33,7 +33,7 @@ RSpec.describe 'CarrierWave::Storage::Fog::File' do
end
describe '#copy_to' do
- let(:dest_filename) { 'copied.txt'}
+ let(:dest_filename) { 'copied.txt' }
it 'copies the file' do
fog_file = subject.send(:file)
@@ -67,7 +67,7 @@ RSpec.describe 'CarrierWave::Storage::Fog::File' do
end
describe '#copy_to' do
- let(:dest_filename) { 'copied.txt'}
+ let(:dest_filename) { 'copied.txt' }
it 'copies the file' do
result = subject.copy_to(dest_filename)
diff --git a/spec/initializers/load_balancing_spec.rb b/spec/initializers/load_balancing_spec.rb
new file mode 100644
index 00000000000..d9162acd2cd
--- /dev/null
+++ b/spec/initializers/load_balancing_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'load_balancing', :delete, :reestablished_active_record_base do
+ subject(:initialize_load_balancer) do
+ load Rails.root.join('config/initializers/load_balancing.rb')
+ end
+
+ before do
+ # Stub out middleware call, as not idempotent
+ allow(Gitlab::Application.instance.middleware).to receive(:use)
+ end
+
+ context 'with replica hosts configured' do
+ before do
+ # Setup host-based load balancing
+ # Patch in our load balancer config, simply pointing at the test database twice
+ allow(Gitlab::Database::LoadBalancing::Configuration).to receive(:for_model) do |base_model|
+ db_host = base_model.connection_pool.db_config.host
+
+ Gitlab::Database::LoadBalancing::Configuration.new(base_model, [db_host, db_host])
+ end
+ end
+
+ after do
+ # reset load balancing to original state
+ allow(Gitlab::Database::LoadBalancing::Configuration).to receive(:for_model).and_call_original
+ allow(Gitlab::Cluster::LifecycleEvents).to receive(:in_clustered_puma?).and_call_original
+
+ load Rails.root.join('config/initializers/load_balancing.rb')
+ end
+
+ it 'configures load balancer with two replica hosts' do
+ expect(ApplicationRecord.connection.load_balancer.configuration.hosts.size).to eq(0)
+ expect(Ci::ApplicationRecord.connection.load_balancer.configuration.hosts.size).to eq(0)
+
+ initialize_load_balancer
+
+ expect(ApplicationRecord.connection.load_balancer.configuration.hosts.size).to eq(2)
+ expect(Ci::ApplicationRecord.connection.load_balancer.configuration.hosts.size).to eq(2)
+ end
+
+ context 'for a clustered puma worker' do
+ let!(:group) { create(:group, name: 'my group') }
+
+ before do
+ # Pretend we are in clustered environment
+ allow(Gitlab::Cluster::LifecycleEvents).to receive(:in_clustered_puma?).and_return(true)
+ end
+
+ it 'configures load balancer to have two replica hosts' do
+ initialize_load_balancer
+
+ simulate_puma_worker do
+ expect(ApplicationRecord.connection.load_balancer.configuration.hosts.size).to eq(2)
+ expect(Ci::ApplicationRecord.connection.load_balancer.configuration.hosts.size).to eq(2)
+ end
+ end
+
+ # We tried using Process.fork for a more realistic simulation
+ # but run into bugs where GPRC cannot be used before forking processes.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/333184#note_1081658113
+ def simulate_puma_worker
+ # Called in https://github.com/rails/rails/blob/6-1-stable/activerecord/lib/active_record/connection_adapters/pool_config.rb#L73
+ ActiveRecord::ConnectionAdapters::PoolConfig.discard_pools!
+
+ # Called in config/puma.rb
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+
+ yield
+ end
+
+ it 'makes a read query successfully' do
+ # Clear any previous sticky writes
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+
+ initialize_load_balancer
+
+ group_name = simulate_puma_worker do
+ Group.find_by_name('my group').name
+ end
+
+ expect(group_name).to eq(group.name)
+ end
+
+ it 'makes a write query successfully' do
+ initialize_load_balancer
+
+ expect do
+ simulate_puma_worker do
+ Group.touch_all
+ end
+
+ group.reload
+ end.to change(group, :updated_at)
+ end
+ end
+ end
+end
diff --git a/spec/initializers/microsoft_graph_mailer_spec.rb b/spec/initializers/microsoft_graph_mailer_spec.rb
new file mode 100644
index 00000000000..fbe667e34fe
--- /dev/null
+++ b/spec/initializers/microsoft_graph_mailer_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'microsoft_graph_mailer initializer for GitLab' do
+ let(:microsoft_graph_setting) do
+ {
+ user_id: SecureRandom.hex,
+ tenant: SecureRandom.hex,
+ client_id: SecureRandom.hex,
+ client_secret: SecureRandom.hex,
+ azure_ad_endpoint: 'https://test-azure_ad_endpoint',
+ graph_endpoint: 'https://test-graph_endpoint'
+ }
+ end
+
+ def load_microsoft_graph_mailer_initializer
+ load Rails.root.join('config/initializers/microsoft_graph_mailer.rb')
+ end
+
+ context 'when microsoft_graph_mailer is enabled' do
+ before do
+ stub_microsoft_graph_mailer_setting(microsoft_graph_setting.merge(enabled: true))
+ end
+
+ it 'configures ActionMailer' do
+ previous_delivery_method = ActionMailer::Base.delivery_method
+ previous_microsoft_graph_settings = ActionMailer::Base.microsoft_graph_settings
+
+ load_microsoft_graph_mailer_initializer
+
+ expect(ActionMailer::Base.delivery_method).to eq(:microsoft_graph)
+ expect(ActionMailer::Base.microsoft_graph_settings).to eq(microsoft_graph_setting)
+ ensure
+ ActionMailer::Base.delivery_method = previous_delivery_method
+ ActionMailer::Base.microsoft_graph_settings = previous_microsoft_graph_settings
+ end
+ end
+
+ context 'when microsoft_graph_mailer is disabled' do
+ before do
+ stub_microsoft_graph_mailer_setting(microsoft_graph_setting.merge(enabled: false))
+ end
+
+ it 'does not configure ActionMailer' do
+ previous_delivery_method = ActionMailer::Base.delivery_method
+ previous_microsoft_graph_settings = ActionMailer::Base.microsoft_graph_settings
+
+ load_microsoft_graph_mailer_initializer
+
+ expect(previous_microsoft_graph_settings).not_to eq(:microsoft_graph)
+ expect(ActionMailer::Base.delivery_method).to eq(previous_delivery_method)
+ expect(ActionMailer::Base.microsoft_graph_settings).to eq(previous_microsoft_graph_settings)
+ end
+ end
+end
diff --git a/spec/initializers/net_http_patch_spec.rb b/spec/initializers/net_http_patch_spec.rb
index d6b003d84fa..d56730917f1 100644
--- a/spec/initializers/net_http_patch_spec.rb
+++ b/spec/initializers/net_http_patch_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'net/http'
require_relative '../../config/initializers/net_http_patch'
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index 71ea12a41aa..c3200d2fab1 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -58,4 +58,40 @@ RSpec.describe Settings do
end
end
end
+
+ describe "#weak_passwords_digest_set" do
+ subject { described_class.gitlab.weak_passwords_digest_set }
+
+ it 'is a Set' do
+ expect(subject).to be_kind_of(Set)
+ end
+
+ it 'contains 4500 password digests' do
+ expect(subject.length).to eq(4500)
+ end
+
+ it 'includes 8 char weak password digest' do
+ expect(subject).to include(digest("password"))
+ end
+
+ it 'includes 16 char weak password digest' do
+ expect(subject).to include(digest("progressivehouse"))
+ end
+
+ it 'includes long char weak password digest' do
+ expect(subject).to include(digest("01234567890123456789"))
+ end
+
+ it 'does not include 7 char weak password digest' do
+ expect(subject).not_to include(digest("1234567"))
+ end
+
+ it 'does not include plaintext' do
+ expect(subject).not_to include("password")
+ end
+
+ def digest(plaintext)
+ Digest::SHA256.base64digest(plaintext)
+ end
+ end
end
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 2786f034969..63c96ce17d1 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe 'trusted_proxies' do
end
def stub_request(headers = {})
- ActionDispatch::RemoteIp.new(proc { }, false, Rails.application.config.action_dispatch.trusted_proxies).call(headers)
+ ActionDispatch::RemoteIp.new(proc {}, false, Rails.application.config.action_dispatch.trusted_proxies).call(headers)
ActionDispatch::Request.new(headers)
end
diff --git a/spec/lib/api/entities/ci/job_request/image_spec.rb b/spec/lib/api/entities/ci/job_request/image_spec.rb
index fca3b5d3fa9..14d4a074fce 100644
--- a/spec/lib/api/entities/ci/job_request/image_spec.rb
+++ b/spec/lib/api/entities/ci/job_request/image_spec.rb
@@ -32,14 +32,4 @@ RSpec.describe API::Entities::Ci::JobRequest::Image do
it 'returns the pull policy' do
expect(subject[:pull_policy]).to eq(['if-not-present'])
end
-
- context 'when the FF ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it 'does not return the pull policy' do
- expect(subject).not_to have_key(:pull_policy)
- end
- end
end
diff --git a/spec/lib/api/entities/ci/job_request/service_spec.rb b/spec/lib/api/entities/ci/job_request/service_spec.rb
index 86f2120c321..11350f7c41b 100644
--- a/spec/lib/api/entities/ci/job_request/service_spec.rb
+++ b/spec/lib/api/entities/ci/job_request/service_spec.rb
@@ -40,12 +40,4 @@ RSpec.describe API::Entities::Ci::JobRequest::Service do
expect(subject[:ports]).to be_nil
end
end
-
- context 'when the FF ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it { is_expected.not_to have_key(:pull_policy) }
- end
end
diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
new file mode 100644
index 00000000000..2a6d0825e5c
--- /dev/null
+++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
+ let_it_be(:candidate) { create(:ml_candidates) }
+
+ subject { described_class.new(candidate).as_json }
+
+ context 'when start_time is nil' do
+ it { expect(subject[:start_time]).to eq(0) }
+ end
+
+ context 'when start_time is not nil' do
+ before do
+ allow(candidate).to receive(:start_time).and_return(1234)
+ end
+
+ it { expect(subject[:start_time]).to eq(1234) }
+ end
+
+ describe 'end_time' do
+ context 'when nil' do
+ it { is_expected.not_to have_key(:end_time) }
+ end
+
+ context 'when not nil' do
+ before do
+ allow(candidate).to receive(:end_time).and_return(1234)
+ end
+
+ it { expect(subject[:end_time]).to eq(1234) }
+ end
+ end
+
+ describe 'experiment_id' do
+ it 'is the experiment iid as string' do
+ expect(subject[:experiment_id]).to eq(candidate.experiment.iid.to_s)
+ end
+ end
+
+ describe 'run_id' do
+ it 'is the iid as string' do
+ expect(subject[:run_id]).to eq(candidate.iid.to_s)
+ end
+ end
+
+ describe 'run_uuid' do
+ it 'is the iid as string' do
+ expect(subject[:run_uuid]).to eq(candidate.iid.to_s)
+ end
+ end
+
+ describe 'artifact_uri' do
+ it 'is not implemented' do
+ expect(subject[:artifact_uri]).to eq('not_implemented')
+ end
+ end
+
+ describe 'lifecycle_stage' do
+ it 'is active' do
+ expect(subject[:lifecycle_stage]).to eq('active')
+ end
+ end
+end
diff --git a/spec/lib/api/entities/ml/mlflow/run_spec.rb b/spec/lib/api/entities/ml/mlflow/run_spec.rb
new file mode 100644
index 00000000000..84234f474f5
--- /dev/null
+++ b/spec/lib/api/entities/ml/mlflow/run_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::Ml::Mlflow::Run do
+ let_it_be(:candidate) { create(:ml_candidates) }
+
+ subject { described_class.new(candidate).as_json }
+
+ it 'has run key' do
+ expect(subject).to have_key(:run)
+ end
+
+ it 'has the id' do
+ expect(subject[:run][:info][:run_id]).to eq(candidate.iid.to_s)
+ end
+
+ it 'data is empty' do
+ expect(subject[:run][:data]).to be_empty
+ end
+end
diff --git a/spec/lib/api/entities/personal_access_token_with_details_spec.rb b/spec/lib/api/entities/personal_access_token_with_details_spec.rb
deleted file mode 100644
index a53d6febba1..00000000000
--- a/spec/lib/api/entities/personal_access_token_with_details_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::Entities::PersonalAccessTokenWithDetails do
- describe '#as_json' do
- let_it_be(:user) { create(:user) }
- let_it_be(:token) { create(:personal_access_token, user: user, expires_at: nil) }
-
- let(:entity) { described_class.new(token) }
-
- it 'returns token data' do
- expect(entity.as_json).to eq({
- id: token.id,
- name: token.name,
- revoked: false,
- created_at: token.created_at,
- scopes: ['api'],
- user_id: user.id,
- last_used_at: nil,
- active: true,
- expires_at: nil,
- expired: false,
- expires_soon: false,
- revoke_path: Gitlab::Routing.url_helpers.revoke_profile_personal_access_token_path(token)
- })
- end
- end
-end
diff --git a/spec/lib/api/helpers/caching_spec.rb b/spec/lib/api/helpers/caching_spec.rb
index 38b7b386d5c..828af7b5f91 100644
--- a/spec/lib/api/helpers/caching_spec.rb
+++ b/spec/lib/api/helpers/caching_spec.rb
@@ -33,10 +33,7 @@ RSpec.describe API::Helpers::Caching, :use_clean_rails_redis_caching do
end
describe "#present_cached" do
- subject do
- instance.present_cached(presentable, **kwargs)
- end
-
+ let(:method) { :present_cached }
let(:kwargs) do
{
with: presenter,
@@ -44,6 +41,10 @@ RSpec.describe API::Helpers::Caching, :use_clean_rails_redis_caching do
}
end
+ subject do
+ instance.public_send(method, presentable, **kwargs)
+ end
+
context 'single object' do
let_it_be(:presentable) { create(:todo, project: project) }
diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
index ae0c0f53acd..aa4b0a137cd 100644
--- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
describe '#redirect_registry_request' do
using RSpec::Parameterized::TableSyntax
+ let_it_be(:project) { create(:project) }
+
let(:options) { {} }
subject { helper.redirect_registry_request(forward_to_registry, package_type, options) { helper.fallback } }
@@ -18,8 +20,8 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
shared_examples 'executing fallback' do
it 'redirects to package registry' do
- expect(helper).to receive(:registry_url).never
- expect(helper).to receive(:redirect).never
+ expect(helper).not_to receive(:registry_url)
+ expect(helper).not_to receive(:redirect)
expect(helper).to receive(:fallback).once
subject
@@ -30,7 +32,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
it 'redirects to package registry', :snowplow do
expect(helper).to receive(:registry_url).once
expect(helper).to receive(:redirect).once
- expect(helper).to receive(:fallback).never
+ expect(helper).not_to receive(:fallback)
subject
@@ -38,11 +40,12 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
end
end
- %i[npm pypi].each do |forwardable_package_type|
+ %i[maven npm pypi].each do |forwardable_package_type|
context "with #{forwardable_package_type} packages" do
include_context 'dependency proxy helpers context'
let(:package_type) { forwardable_package_type }
+ let(:options) { { project: project } }
where(:application_setting, :forward_to_registry, :example_name) do
true | true | 'executing redirect'
@@ -59,17 +62,41 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
it_behaves_like params[:example_name]
end
end
+
+ context 'when maven_central_request_forwarding is disabled' do
+ let(:package_type) { :maven }
+ let(:options) { { project: project } }
+
+ include_context 'dependency proxy helpers context'
+
+ where(:application_setting, :forward_to_registry) do
+ true | true
+ true | false
+ false | true
+ false | false
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(maven_central_request_forwarding: false)
+ allow_fetch_application_setting(attribute: "maven_package_requests_forwarding", return_value: application_setting)
+ end
+
+ it_behaves_like 'executing fallback'
+ end
+ end
end
context 'with non-forwardable package type' do
let(:forward_to_registry) { true }
before do
+ stub_application_setting(maven_package_requests_forwarding: true)
stub_application_setting(npm_package_requests_forwarding: true)
stub_application_setting(pypi_package_requests_forwarding: true)
end
- Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
+ Packages::Package.package_types.keys.without('maven', 'npm', 'pypi').each do |pkg_type|
context "#{pkg_type}" do
let(:package_type) { pkg_type.to_sym }
@@ -81,18 +108,21 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
end
describe '#registry_url' do
- subject { helper.registry_url(package_type, package_name: 'test') }
+ subject { helper.registry_url(package_type, options) }
- where(:package_type, :expected_result) do
- :npm | 'https://registry.npmjs.org/test'
- :pypi | 'https://pypi.org/simple/test/'
+ where(:package_type, :expected_result, :params) do
+ :maven | 'https://repo.maven.apache.org/maven2/test/123' | { path: 'test', file_name: '123', project: project }
+ :npm | 'https://registry.npmjs.org/test' | { package_name: 'test' }
+ :pypi | 'https://pypi.org/simple/test/' | { package_name: 'test' }
end
with_them do
+ let(:options) { params }
+
it { is_expected.to eq(expected_result) }
end
- Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
+ Packages::Package.package_types.keys.without('maven', 'npm', 'pypi').each do |pkg_type|
context "with non-forwardable package type #{pkg_type}" do
let(:package_type) { pkg_type }
diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb
index 0c51e25bad9..cd6e718ce98 100644
--- a/spec/lib/api/helpers/packages_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages_helpers_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe API::Helpers::PackagesHelpers do
let_it_be(:helper) { Class.new.include(described_class).new }
let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:package) { create(:package) }
describe 'authorize_packages_access!' do
subject { helper.authorize_packages_access!(project) }
@@ -17,7 +19,45 @@ RSpec.describe API::Helpers::PackagesHelpers do
end
end
- %i[read_package create_package destroy_package].each do |action|
+ describe 'authorize_read_package!' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:subject, :expected_class) do
+ ref(:project) | ::Packages::Policies::Project
+ ref(:group) | ::Packages::Policies::Group
+ ref(:package) | ::Packages::Package
+ end
+
+ with_them do
+ it 'calls authorize! with correct subject' do
+ expect(helper).to receive(:authorize!).with(:read_package, have_attributes(id: subject.id, class: expected_class))
+
+ expect(helper.send('authorize_read_package!', subject)).to eq nil
+ end
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(read_package_policy_rule: false)
+ end
+
+ where(:subject, :expected_class) do
+ ref(:project) | ::Project
+ ref(:group) | ::Group
+ ref(:package) | ::Packages::Package
+ end
+
+ with_them do
+ it 'calls authorize! with correct subject' do
+ expect(helper).to receive(:authorize!).with(:read_package, have_attributes(id: subject.id, class: expected_class))
+
+ expect(helper.send('authorize_read_package!', subject)).to eq nil
+ end
+ end
+ end
+ end
+
+ %i[create_package destroy_package].each do |action|
describe "authorize_#{action}!" do
subject { helper.send("authorize_#{action}!", project) }
@@ -40,7 +80,7 @@ RSpec.describe API::Helpers::PackagesHelpers do
context 'with packages enabled' do
it "doesn't call not_found!" do
- expect(helper).to receive(:not_found!).never
+ expect(helper).not_to receive(:not_found!)
expect(subject).to eq nil
end
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index a008c1adeac..ae6af5b540e 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe API::Helpers::Pagination do
subject { Class.new.include(described_class).new }
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index cd41d362d03..f25c75ef93c 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -865,4 +865,93 @@ RSpec.describe API::Helpers do
helper.bad_request!('custom reason')
end
end
+
+ describe '#authenticate_by_gitlab_shell_token!' do
+ include GitlabShellHelpers
+
+ let(:valid_secret_token) { 'valid' }
+ let(:invalid_secret_token) { 'invalid' }
+ let(:headers) { {} }
+ let(:params) { {} }
+
+ shared_examples 'authorized' do
+ it 'authorized' do
+ expect(helper).not_to receive(:unauthorized!)
+
+ helper.authenticate_by_gitlab_shell_token!
+ end
+ end
+
+ shared_examples 'unauthorized' do
+ it 'unauthorized' do
+ expect(helper).to receive(:unauthorized!)
+
+ helper.authenticate_by_gitlab_shell_token!
+ end
+ end
+
+ before do
+ allow(Gitlab::Shell).to receive(:secret_token).and_return(valid_secret_token)
+ allow(helper).to receive_messages(params: params, headers: headers, secret_token: valid_secret_token)
+ end
+
+ context 'when jwt token is not provided' do
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'when jwt token is invalid' do
+ let(:headers) { gitlab_shell_internal_api_request_header(secret_token: invalid_secret_token) }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'when jwt token issuer is invalid' do
+ let(:headers) { gitlab_shell_internal_api_request_header(issuer: 'gitlab-workhorse') }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'when jwt token is valid' do
+ let(:headers) { gitlab_shell_internal_api_request_header }
+
+ it_behaves_like 'authorized'
+ end
+
+ context 'when gitlab_shell_jwt_token is disabled' do
+ let(:valid_secret_token) { +'valid' } # mutable string to use chomp!
+ let(:invalid_secret_token) { +'invalid' } # mutable string to use chomp!
+
+ before do
+ stub_feature_flags(gitlab_shell_jwt_token: false)
+ end
+
+ context 'when shared secret is not provided' do
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'when shared secret provided via params' do
+ let(:params) { { 'secret_token' => valid_secret_token } }
+
+ it_behaves_like 'authorized'
+
+ context 'but it is invalid' do
+ let(:params) { { 'secret_token' => invalid_secret_token } }
+
+ it_behaves_like 'unauthorized'
+ end
+ end
+
+ context 'when shared secret provided via headers' do
+ let(:headers) { { described_class::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(valid_secret_token) } }
+
+ it_behaves_like 'authorized'
+
+ context 'but it is invalid' do
+ let(:headers) { { described_class::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(invalid_secret_token) } }
+
+ it_behaves_like 'unauthorized'
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/api/integrations/slack/events/url_verification_spec.rb b/spec/lib/api/integrations/slack/events/url_verification_spec.rb
deleted file mode 100644
index 2778f0d708d..00000000000
--- a/spec/lib/api/integrations/slack/events/url_verification_spec.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::Integrations::Slack::Events::UrlVerification do
- describe '.call' do
- it 'returns the challenge' do
- expect(described_class.call({ challenge: 'foo' })).to eq({ challenge: 'foo' })
- end
- end
-end
diff --git a/spec/lib/backup/database_backup_error_spec.rb b/spec/lib/backup/database_backup_error_spec.rb
index ef627900050..e001f65465c 100644
--- a/spec/lib/backup/database_backup_error_spec.rb
+++ b/spec/lib/backup/database_backup_error_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Backup::DatabaseBackupError do
let(:config) do
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index d427e41026e..6b0747735ed 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Backup::GitalyBackup do
let(:expected_env) do
{
'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
- 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
+ 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
}.merge(ENV)
end
@@ -121,7 +121,7 @@ RSpec.describe Backup::GitalyBackup do
let(:ssl_env) do
{
'SSL_CERT_FILE' => '/some/cert/file',
- 'SSL_CERT_DIR' => '/some/cert'
+ 'SSL_CERT_DIR' => '/some/cert'
}
end
diff --git a/spec/lib/backup/task_spec.rb b/spec/lib/backup/task_spec.rb
index 80f1fe01b78..1de99729512 100644
--- a/spec/lib/backup/task_spec.rb
+++ b/spec/lib/backup/task_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Backup::Task do
let(:progress) { StringIO.new }
diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb
index 95b3955d8fe..3914aee2d4c 100644
--- a/spec/lib/banzai/color_parser_spec.rb
+++ b/spec/lib/banzai/color_parser_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Banzai::ColorParser do
describe '.parse' do
diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
index 2d326bd77a6..5712ed7da1f 100644
--- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
+++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
@@ -14,6 +14,10 @@ RSpec.describe Banzai::Filter::BlockquoteFenceFilter do
expect(output).to eq(expected)
end
+ it 'does not require newlines at start or end of string' do
+ expect(filter(">>>\ntest\n>>>")).to eq("\n> test\n")
+ end
+
it 'allows trailing whitespace on blockquote fence lines' do
expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n")
end
diff --git a/spec/lib/banzai/filter/kroki_filter_spec.rb b/spec/lib/banzai/filter/kroki_filter_spec.rb
index 1fb61ad1991..3f4f3aafdd6 100644
--- a/spec/lib/banzai/filter/kroki_filter_spec.rb
+++ b/spec/lib/banzai/filter/kroki_filter_spec.rb
@@ -46,4 +46,12 @@ RSpec.describe Banzai::Filter::KrokiFilter do
expect(doc.to_s).to start_with '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KyJyVNQiE5KTSxKidXVjS5ILCrKL4lFFrSyi07LL81RyM0vLckAysRGjxo8avCowaMGjxo8avCowaMGU8lgAE7mIdc=" hidden="" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDog'
end
+
+ it 'allows the lang attribute on the code tag to support RST files processed by gitlab-markup gem' do
+ stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000")
+ text = '[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]' * 25
+ doc = filter("<pre><code lang='nomnoml'>#{text}</code></pre>")
+
+ expect(doc.to_s).to start_with '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KyJyVNQiE5KTSxKidXVjS5ILCrKL4lFFrSyi07LL81RyM0vLckAysRGjxo8avCowaMGjxo8avCowaMGU8lgAE7mIdc=" hidden="" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDog'
+ end
end
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index 128f8532d39..dd116eb1109 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -3,128 +3,179 @@
require 'spec_helper'
RSpec.describe Banzai::Filter::MathFilter do
+ using RSpec::Parameterized::TableSyntax
include FilterSpecHelper
- it 'leaves regular inline code unchanged' do
- input = "<code>2+2</code>"
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
- doc = filter("$<code>2+2</code>$")
-
- expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>'
- end
-
- it 'only removes surrounding dollar signs' do
- doc = filter("test $<code>2+2</code>$ test")
- before = doc.xpath('descendant-or-self::text()[1]').first
- after = doc.xpath('descendant-or-self::text()[3]').first
-
- expect(before.to_s).to eq 'test '
- expect(after.to_s).to eq ' test'
- end
-
- it 'only removes surrounding single dollar sign' do
- doc = filter("test $$<code>2+2</code>$$ test")
- before = doc.xpath('descendant-or-self::text()[1]').first
- after = doc.xpath('descendant-or-self::text()[3]').first
-
- expect(before.to_s).to eq 'test $'
- expect(after.to_s).to eq '$ test'
- end
-
- it 'adds data-math-style inline attribute to inline math' do
- doc = filter('$<code>2+2</code>$')
- code = doc.xpath('descendant-or-self::code').first
-
- expect(code['data-math-style']).to eq 'inline'
- end
-
- it 'adds class code and math to inline math' do
- doc = filter('$<code>2+2</code>$')
- code = doc.xpath('descendant-or-self::code').first
-
- expect(code[:class]).to include("code")
- expect(code[:class]).to include("math")
- end
-
- it 'adds js-render-math class to inline math' do
- doc = filter('$<code>2+2</code>$')
- code = doc.xpath('descendant-or-self::code').first
-
- expect(code[:class]).to include("js-render-math")
- end
-
- # Cases with faulty syntax. Should be a no-op
-
- it 'ignores cases with missing dolar sign at the end' do
- input = "test $<code>2+2</code> test"
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- it 'ignores cases with missing dolar sign at the beginning' do
- input = "test <code>2+2</code>$ test"
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- it 'ignores dollar signs if it is not adjacent' do
- input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>'
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- it 'ignores dollar signs if they are inside another element' do
- input = '<p>We check strictly <em>$</em><code>2+2</code><em>$</em></p>'
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- # Display math
-
- it 'adds data-math-style display attribute to display math' do
- doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
- pre = doc.xpath('descendant-or-self::pre').first
-
- expect(pre['data-math-style']).to eq 'display'
- end
-
- it 'adds js-render-math class to display math' do
- doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
- pre = doc.xpath('descendant-or-self::pre').first
-
- expect(pre[:class]).to include("js-render-math")
- end
-
- it 'ignores code blocks that are not math' do
- input = '<pre class="code highlight js-syntax-highlight language-plaintext" v-pre="true"><code>2+2</code></pre>'
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- it 'requires the pre to contain both code and math' do
- input = '<pre class="highlight js-syntax-highlight language-plaintext language-math" v-pre="true"><code>2+2</code></pre>'
- doc = filter(input)
-
- expect(doc.to_s).to eq input
- end
-
- it 'dollar signs around to display math' do
- doc = filter('$<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>$')
- before = doc.xpath('descendant-or-self::text()[1]').first
- after = doc.xpath('descendant-or-self::text()[3]').first
-
- expect(before.to_s).to eq '$'
- expect(after.to_s).to eq '$'
+ shared_examples 'inline math' do
+ it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
+ doc = filter(text)
+ expected = result_template.gsub('<math>', '<code class="code math js-render-math" data-math-style="inline">')
+ expected.gsub!('</math>', '</code>')
+
+ expect(doc.to_s).to eq expected
+ end
+ end
+
+ shared_examples 'display math' do
+ let_it_be(:template_prefix_with_pre) { '<pre class="code math js-render-math" data-math-style="display"><code>' }
+ let_it_be(:template_prefix_with_code) { '<code class="code math js-render-math" data-math-style="display">' }
+ let(:use_pre_tags) { false }
+
+ it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
+ doc = filter(text)
+
+ template_prefix = use_pre_tags ? template_prefix_with_pre : template_prefix_with_code
+ template_suffix = "</code>#{'</pre>' if use_pre_tags}"
+ expected = result_template.gsub('<math>', template_prefix)
+ expected.gsub!('</math>', template_suffix)
+
+ expect(doc.to_s).to eq expected
+ end
+ end
+
+ describe 'inline math using $...$ syntax' do
+ context 'with valid syntax' do
+ where(:text, :result_template) do
+ '$2+2$' | '<math>2+2</math>'
+ '$22+1$ and $22 + a^2$' | '<math>22+1</math> and <math>22 + a^2</math>'
+ '$22 and $2+2$' | '$22 and <math>2+2</math>'
+ '$2+2$ $22 and flightjs/Flight$22 $2+2$' | '<math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math>'
+ '$1/2$ &lt;b&gt;test&lt;/b&gt;' | '<math>1/2</math> &lt;b&gt;test&lt;/b&gt;'
+ '$a!$' | '<math>a!</math>'
+ '$x$' | '<math>x</math>'
+ end
+
+ with_them do
+ it_behaves_like 'inline math'
+ end
+ end
+
+ it 'does not handle dollar literals properly' do
+ doc = filter('$20+30\$$')
+ expected = '<code class="code math js-render-math" data-math-style="inline">20+30\\</code>$'
+
+ expect(doc.to_s).to eq expected
+ end
+ end
+
+ describe 'inline math using $`...`$ syntax' do
+ context 'with valid syntax' do
+ where(:text, :result_template) do
+ '$<code>2+2</code>$' | '<math>2+2</math>'
+ '$<code>22+1</code>$ and $<code>22 + a^2</code>$' | '<math>22+1</math> and <math>22 + a^2</math>'
+ '$22 and $<code>2+2</code>$' | '$22 and <math>2+2</math>'
+ '$<code>2+2</code>$ $22 and flightjs/Flight$22 $<code>2+2</code>$' | '<math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math>'
+ 'test $$<code>2+2</code>$$ test' | 'test $<math>2+2</math>$ test'
+ end
+
+ with_them do
+ it_behaves_like 'inline math'
+ end
+ end
+ end
+
+ describe 'inline display math using $$...$$ syntax' do
+ context 'with valid syntax' do
+ where(:text, :result_template) do
+ '$$2+2$$' | '<math>2+2</math>'
+ '$$ 2+2 $$' | '<math>2+2</math>'
+ '$$22+1$$ and $$22 + a^2$$' | '<math>22+1</math> and <math>22 + a^2</math>'
+ '$22 and $$2+2$$' | '$22 and <math>2+2</math>'
+ '$$2+2$$ $22 and flightjs/Flight$22 $$2+2$$' | '<math>2+2</math> $22 and flightjs/Flight$22 <math>2+2</math>'
+ 'flightjs/Flight$22 and $$a^2 + b^2 = c^2$$' | 'flightjs/Flight$22 and <math>a^2 + b^2 = c^2</math>'
+ '$$a!$$' | '<math>a!</math>'
+ '$$x$$' | '<math>x</math>'
+ '$$20,000 and $$30,000' | '<math>20,000 and</math>30,000'
+ end
+
+ with_them do
+ it_behaves_like 'display math'
+ end
+ end
+ end
+
+ describe 'block display math using $$\n...\n$$ syntax' do
+ context 'with valid syntax' do
+ where(:text, :result_template) do
+ "$$\n2+2\n$$" | "<math>2+2</math>"
+ end
+
+ with_them do
+ it_behaves_like 'display math' do
+ let(:use_pre_tags) { true }
+ end
+ end
+ end
+ end
+
+ describe 'display math using ```math...``` syntax' do
+ it 'adds data-math-style display attribute to display math' do
+ doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
+ pre = doc.xpath('descendant-or-self::pre').first
+
+ expect(pre['data-math-style']).to eq 'display'
+ end
+
+ it 'adds js-render-math class to display math' do
+ doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
+ pre = doc.xpath('descendant-or-self::pre').first
+
+ expect(pre[:class]).to include("js-render-math")
+ end
+
+ it 'ignores code blocks that are not math' do
+ input = '<pre class="code highlight js-syntax-highlight language-plaintext" v-pre="true"><code>2+2</code></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'requires the pre to contain both code and math' do
+ input = '<pre class="highlight js-syntax-highlight language-plaintext language-math" v-pre="true"><code>2+2</code></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'dollar signs around to display math' do
+ doc = filter('$<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>$')
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq '$'
+ expect(after.to_s).to eq '$'
+ end
+ end
+
+ describe 'unrecognized syntax' do
+ where(:text) do
+ [
+ '<code>2+2</code>',
+ 'test $<code>2+2</code> test',
+ 'test <code>2+2</code>$ test',
+ '<em>$</em><code>2+2</code><em>$</em>',
+ '$20,000 and $30,000',
+ '$20,000 in $USD',
+ '$ a^2 $',
+ "test $$\n2+2\n$$",
+ "$\n$",
+ '$$$'
+ ]
+ end
+
+ with_them do
+ it 'is ignored' do
+ expect(filter(text).to_s).to eq text
+ end
+ end
+ end
+
+ it 'handles multiple styles in one text block' do
+ doc = filter('$<code>2+2</code>$ + $3+3$ + $$4+4$$')
+
+ expect(doc.search('.js-render-math').count).to eq(3)
+ expect(doc.search('[data-math-style="inline"]').count).to eq(2)
+ expect(doc.search('[data-math-style="display"]').count).to eq(1)
end
it 'limits how many elements can be marked as math' do
@@ -134,4 +185,11 @@ RSpec.describe Banzai::Filter::MathFilter do
expect(doc.search('.js-render-math').count).to eq(2)
end
+
+ it 'does not recognize new syntax when feature flag is off' do
+ stub_feature_flags(markdown_dollar_math: false)
+ doc = filter('$1+2$')
+
+ expect(doc.to_s).to eq '$1+2$'
+ end
end
diff --git a/spec/lib/banzai/filter/output_safety_spec.rb b/spec/lib/banzai/filter/output_safety_spec.rb
index 5b7b7298411..8186935f4b2 100644
--- a/spec/lib/banzai/filter/output_safety_spec.rb
+++ b/spec/lib/banzai/filter/output_safety_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Banzai::Filter::OutputSafety do
subject do
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index dcfeb2ce3ba..4373af90cde 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -15,6 +15,16 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
expect(doc.to_s).to eq output
end
+ it 'allows the lang attribute on the code tag to support RST files processed by gitlab-markup gem' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
+
+ input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
+ output = '<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
it 'does not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false)
diff --git a/spec/lib/banzai/filter/repository_link_filter_spec.rb b/spec/lib/banzai/filter/repository_link_filter_spec.rb
index 815053aac2f..c220263b238 100644
--- a/spec/lib/banzai/filter/repository_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/repository_link_filter_spec.rb
@@ -8,14 +8,14 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter do
def filter(doc, contexts = {})
contexts.reverse_merge!({
- commit: commit,
- project: project,
- current_user: user,
- group: group,
- wiki: wiki,
- ref: ref,
+ commit: commit,
+ project: project,
+ current_user: user,
+ group: group,
+ wiki: wiki,
+ ref: ref,
requested_path: requested_path,
- only_path: only_path
+ only_path: only_path
})
described_class.call(doc, contexts)
diff --git a/spec/lib/banzai/filter_array_spec.rb b/spec/lib/banzai/filter_array_spec.rb
index 47bc5633300..f341d5d51a0 100644
--- a/spec/lib/banzai/filter_array_spec.rb
+++ b/spec/lib/banzai/filter_array_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Banzai::FilterArray do
describe '#insert_after' do
diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
index 5021ef3a79a..303d0fcb6c2 100644
--- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
@@ -32,4 +32,21 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do
expect(result[:output]).to eq('foo foo f...')
end
+
+ context 'when multiline blockquote' do
+ it 'data-sourcepos references correct line in source markdown' do
+ markdown = <<~MD
+ >>>
+ foo
+ >>>
+ MD
+
+ pipeline_output = described_class.call(markdown, {})[:output]
+ pipeline_output = Banzai::Pipeline::PlainMarkdownPipeline.call(pipeline_output, {})[:output]
+ sourcepos = pipeline_output.at('blockquote')['data-sourcepos']
+ source_line = sourcepos.split(':').first.to_i
+
+ expect(markdown.lines[source_line - 1]).to eq "foo\n"
+ end
+ end
end
diff --git a/spec/lib/banzai/pipeline_spec.rb b/spec/lib/banzai/pipeline_spec.rb
index 7d4df2ca5ce..b2c970e4394 100644
--- a/spec/lib/banzai/pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Banzai::Pipeline do
describe '.[]' do
diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb
index b76f6ec533c..fc7aaa94954 100644
--- a/spec/lib/banzai/querying_spec.rb
+++ b/spec/lib/banzai/querying_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Banzai::Querying do
describe '.css' do
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index ae9cf4c5068..705f44baf16 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Banzai::Renderer do
let(:object) { fake_object(fresh: true) }
it 'uses the cache' do
- expect(object).to receive(:refresh_markdown_cache!).never
+ expect(object).not_to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html')
end
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
index 349274585c4..715b78c95eb 100644
--- a/spec/lib/bitbucket/collection_spec.rb
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
# Emulates paginator. It returns 2 pages with results
class TestPaginator
diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb
index 1d599007d9e..46ab5a45551 100644
--- a/spec/lib/bitbucket/page_spec.rb
+++ b/spec/lib/bitbucket/page_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Page do
let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } }
diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb
index e74af8a264b..3285fae5b82 100644
--- a/spec/lib/bitbucket/paginator_spec.rb
+++ b/spec/lib/bitbucket/paginator_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Paginator do
let(:last_page) { double(:page, next?: false, items: ['item_2']) }
diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb
index f6766ab685b..d108bcfe767 100644
--- a/spec/lib/bitbucket/representation/comment_spec.rb
+++ b/spec/lib/bitbucket/representation/comment_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Representation::Comment do
describe '#author' do
diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb
index 8c27086546f..a40bbcb7bf8 100644
--- a/spec/lib/bitbucket/representation/issue_spec.rb
+++ b/spec/lib/bitbucket/representation/issue_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Representation::Issue do
describe '#iid' do
diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
index cdab683492f..e748cd7b955 100644
--- a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
+++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Representation::PullRequestComment do
describe '#iid' do
diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb
index 6f05d03aa0a..87a9a0fa76d 100644
--- a/spec/lib/bitbucket/representation/pull_request_spec.rb
+++ b/spec/lib/bitbucket/representation/pull_request_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Representation::PullRequest do
describe '#iid' do
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
index a779a153f25..b5b9f45f3d4 100644
--- a/spec/lib/bitbucket/representation/repo_spec.rb
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Representation::Repo do
describe '#has_wiki?' do
diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb
index e1f6c724da8..62431a5ad8b 100644
--- a/spec/lib/bitbucket/representation/user_spec.rb
+++ b/spec/lib/bitbucket/representation/user_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Bitbucket::Representation::User do
describe '#username' do
diff --git a/spec/lib/bitbucket_server/page_spec.rb b/spec/lib/bitbucket_server/page_spec.rb
index 2d4e946e590..2837f94ba3e 100644
--- a/spec/lib/bitbucket_server/page_spec.rb
+++ b/spec/lib/bitbucket_server/page_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe BitbucketServer::Page do
let(:response) { { 'values' => [{ 'description' => 'Test' }], 'isLastPage' => false, 'nextPageStart' => 2 } }
diff --git a/spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb b/spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb
new file mode 100644
index 00000000000..a77eba06027
--- /dev/null
+++ b/spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileDownloads::FilenameFetch do
+ let(:dummy_instance) { dummy_class.new }
+ let(:dummy_class) do
+ Class.new do
+ include BulkImports::FileDownloads::FilenameFetch
+ end
+ end
+
+ describe '#raise_error' do
+ it { expect { dummy_instance.raise_error('text') }.to raise_exception(NotImplementedError) }
+ end
+end
diff --git a/spec/lib/bulk_imports/file_downloads/validations_spec.rb b/spec/lib/bulk_imports/file_downloads/validations_spec.rb
new file mode 100644
index 00000000000..85f45c2a8f0
--- /dev/null
+++ b/spec/lib/bulk_imports/file_downloads/validations_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileDownloads::Validations do
+ let(:dummy_instance) { dummy_class.new }
+ let(:dummy_class) do
+ Class.new do
+ include BulkImports::FileDownloads::Validations
+ end
+ end
+
+ describe '#raise_error' do
+ it { expect { dummy_instance.raise_error('text') }.to raise_exception(NotImplementedError) }
+ end
+
+ describe '#filepath' do
+ it { expect { dummy_instance.filepath }.to raise_exception(NotImplementedError) }
+ end
+
+ describe '#response_headers' do
+ it { expect { dummy_instance.response_headers }.to raise_exception(NotImplementedError) }
+ end
+
+ describe '#file_size_limit' do
+ it { expect { dummy_instance.file_size_limit }.to raise_exception(NotImplementedError) }
+ end
+end
diff --git a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
index 9c79b3f4c9e..045908de5c4 100644
--- a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe BulkImports::Pipeline::ExtractedData do
let(:data) { 'data' }
diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb
index e4ecf99dab0..dc169bb8d88 100644
--- a/spec/lib/bulk_imports/pipeline_spec.rb
+++ b/spec/lib/bulk_imports/pipeline_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe BulkImports::Pipeline do
let(:context) { instance_double(BulkImports::Pipeline::Context, tracker: nil) }
diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
index 3f02356b41e..e780cde4ae2 100644
--- a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
let_it_be(:user) { create(:user) }
+ let_it_be(:another_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
@@ -85,6 +86,9 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
describe '#run' do
before do
group.add_owner(user)
+ group.add_maintainer(another_user)
+
+ ::BulkImports::UsersMapper.new(context: context).cache_source_user_id(42, another_user.id)
allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
allow(extractor).to receive(:remove_tmp_dir)
@@ -293,5 +297,52 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
expect(imported_mr.milestone.title).to eq(attributes.dig('milestone', 'title'))
end
end
+
+ context 'user assignments' do
+ let(:attributes) do
+ {
+ key => [
+ {
+ 'user_id' => 22,
+ 'created_at' => '2020-01-07T11:21:21.235Z'
+ },
+ {
+ 'user_id' => 42,
+ 'created_at' => '2020-01-08T12:21:21.235Z'
+ }
+ ]
+ }
+ end
+
+ context 'assignees' do
+ let(:key) { 'merge_request_assignees' }
+
+ it 'imports mr assignees' do
+ assignees = imported_mr.merge_request_assignees
+
+ expect(assignees.pluck(:user_id)).to contain_exactly(user.id, another_user.id)
+ end
+ end
+
+ context 'approvals' do
+ let(:key) { 'approvals' }
+
+ it 'imports mr approvals' do
+ approvals = imported_mr.approvals
+
+ expect(approvals.pluck(:user_id)).to contain_exactly(user.id, another_user.id)
+ end
+ end
+
+ context 'reviewers' do
+ let(:key) { 'merge_request_reviewers' }
+
+ it 'imports mr reviewers' do
+ reviewers = imported_mr.merge_request_reviewers
+
+ expect(reviewers.pluck(:user_id)).to contain_exactly(user.id, another_user.id)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/bulk_imports/retry_pipeline_error_spec.rb b/spec/lib/bulk_imports/retry_pipeline_error_spec.rb
index 9d96407b03a..2ff6a7d2b5c 100644
--- a/spec/lib/bulk_imports/retry_pipeline_error_spec.rb
+++ b/spec/lib/bulk_imports/retry_pipeline_error_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe BulkImports::RetryPipelineError do
describe '#retry_delay' do
diff --git a/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb b/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb
index 70e649d35da..f01703033cc 100644
--- a/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb
+++ b/spec/lib/constraints/jira_encoded_url_constrainer_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Constraints::JiraEncodedUrlConstrainer do
let(:namespace_id) { 'group' }
diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb
index 7836d8706f6..f19bedbda0e 100644
--- a/spec/lib/container_registry/gitlab_api_client_spec.rb
+++ b/spec/lib/container_registry/gitlab_api_client_spec.rb
@@ -421,7 +421,9 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
before do
expect(Auth::ContainerRegistryAuthenticationService).to receive(:pull_nested_repositories_access_token).with(path.downcase).and_return(token)
- stub_repository_details(path, sizing: :self_with_descendants, status_code: 200, respond_with: response)
+ expect_next_instance_of(described_class) do |client|
+ expect(client).to receive(:repository_details).with(path.downcase, sizing: :self_with_descendants).and_return(response.with_indifferent_access).once
+ end
end
it { is_expected.to eq(555) }
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index 190ddef0cd5..cb5c6a60e1d 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -240,6 +240,31 @@ RSpec.describe ContainerRegistry::Tag do
it_behaves_like 'setting and caching the created_at value'
end
end
+
+ describe 'updated_at=' do
+ subject do
+ tag.updated_at = input
+ tag.updated_at
+ end
+
+ context 'with a valid input' do
+ let(:input) { 2.days.ago.iso8601 }
+
+ it { is_expected.to eq(DateTime.iso8601(input)) }
+ end
+
+ context 'with a nil input' do
+ let(:input) { nil }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'with an invalid input' do
+ let(:input) { 'not a timestamp' }
+
+ it { is_expected.to eq(nil) }
+ end
+ end
end
end
end
diff --git a/spec/lib/declarative_enum_spec.rb b/spec/lib/declarative_enum_spec.rb
index 66cda9fc3a8..06e74b639cf 100644
--- a/spec/lib/declarative_enum_spec.rb
+++ b/spec/lib/declarative_enum_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe DeclarativeEnum do
let(:enum_module) do
diff --git a/spec/lib/error_tracking/sentry_client/event_spec.rb b/spec/lib/error_tracking/sentry_client/event_spec.rb
index 64e674f1e9b..d65bfa31018 100644
--- a/spec/lib/error_tracking/sentry_client/event_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/event_spec.rb
@@ -32,6 +32,7 @@ RSpec.describe ErrorTracking::SentryClient do
subject { client.issue_latest_event(issue_id: issue_id) }
it_behaves_like 'calls sentry api'
+ it_behaves_like 'Sentry API response size limit'
it 'has correct return type' do
expect(subject).to be_a(Gitlab::ErrorTracking::ErrorEvent)
@@ -50,7 +51,7 @@ RSpec.describe ErrorTracking::SentryClient do
end
end
- context 'error object created from sentry response' do
+ context 'with error object created from sentry response' do
it_behaves_like 'assigns error tracking event correctly'
it 'parses the stack trace' do
@@ -58,7 +59,7 @@ RSpec.describe ErrorTracking::SentryClient do
expect(subject.stack_trace_entries).not_to be_empty
end
- context 'error without stack trace' do
+ context 'with error without stack trace' do
before do
sample_response['entries'] = []
stub_sentry_request(sentry_request_url, body: sample_response)
diff --git a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
index f86d328ef89..75e7ac8304e 100644
--- a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe ErrorTracking::SentryClient::IssueLink do
let_it_be(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) }
let_it_be(:issue) { create(:issue, project: error_tracking_setting.project) }
+ let(:token) { 'test-token' }
let(:client) { error_tracking_setting.sentry_client }
let(:sentry_issue_id) { 11111111 }
@@ -22,11 +23,12 @@ RSpec.describe ErrorTracking::SentryClient::IssueLink do
subject { client.create_issue_link(integration_id, sentry_issue_id, issue) }
+ it_behaves_like 'Sentry API response size limit'
it_behaves_like 'calls sentry api'
it { is_expected.to be_present }
- context 'redirects' do
+ context 'with redirects' do
let(:sentry_api_url) { sentry_issue_link_url }
it_behaves_like 'no Sentry redirects', :put
@@ -45,11 +47,12 @@ RSpec.describe ErrorTracking::SentryClient::IssueLink do
let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/plugin_link_sample_response.json')) }
let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :post, body: sentry_api_response) }
+ it_behaves_like 'Sentry API response size limit'
it_behaves_like 'calls sentry api'
it { is_expected.to be_present }
- context 'redirects' do
+ context 'with redirects' do
let(:sentry_api_url) { sentry_issue_link_url }
it_behaves_like 'no Sentry redirects', :post
diff --git a/spec/lib/error_tracking/sentry_client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb
index d7bb0ca5c9a..1468a1ff7eb 100644
--- a/spec/lib/error_tracking/sentry_client/issue_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb
@@ -58,6 +58,8 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues have correct length', 3
+ it_behaves_like 'maps Sentry exceptions'
+ it_behaves_like 'Sentry API response size limit', enabled_by_default: true
shared_examples 'has correct external_url' do
describe '#external_url' do
@@ -151,7 +153,7 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
context 'with older sentry versions where keys are not present' do
let(:sentry_api_response) do
- issues_sample_response[0...1].map do |issue|
+ issues_sample_response.first(1).map do |issue|
issue[:project].delete(:id)
issue
end
@@ -167,7 +169,7 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
context 'when essential keys are missing in API response' do
let(:sentry_api_response) do
- issues_sample_response[0...1].map do |issue|
+ issues_sample_response.first(1).map do |issue|
issue.except(:id)
end
end
@@ -178,18 +180,6 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
end
end
- context 'when sentry api response is too large' do
- it 'raises exception' do
- deep_size = instance_double(Gitlab::Utils::DeepSize, valid?: false)
- allow(Gitlab::Utils::DeepSize).to receive(:new).with(sentry_api_response).and_return(deep_size)
-
- expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
- 'Sentry API response is too big. Limit is 1 MB.')
- end
- end
-
- it_behaves_like 'maps Sentry exceptions'
-
context 'when search term is present' do
let(:search_term) { 'NoMethodError' }
let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" }
@@ -219,10 +209,14 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
end
let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" }
- let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: issue_sample_response) }
+ let(:sentry_api_response) { issue_sample_response }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
subject { client.issue_details(issue_id: issue_id) }
+ it_behaves_like 'maps Sentry exceptions'
+ it_behaves_like 'Sentry API response size limit'
+
context 'with error object created from sentry response' do
using RSpec::Parameterized::TableSyntax
@@ -321,6 +315,10 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
subject { client.update_issue(issue_id: issue_id, params: params) }
+ it_behaves_like 'Sentry API response size limit' do
+ let(:sentry_api_response) { {} }
+ end
+
it_behaves_like 'calls sentry api' do
let(:sentry_api_request) { stub_sentry_request(sentry_request_url, :put) }
end
diff --git a/spec/lib/error_tracking/sentry_client/projects_spec.rb b/spec/lib/error_tracking/sentry_client/projects_spec.rb
index 247f9c1c085..52f8cdc915e 100644
--- a/spec/lib/error_tracking/sentry_client/projects_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/projects_spec.rb
@@ -35,10 +35,11 @@ RSpec.describe ErrorTracking::SentryClient::Projects do
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
it_behaves_like 'has correct length', 2
+ it_behaves_like 'Sentry API response size limit'
context 'essential keys missing in API response' do
let(:sentry_api_response) do
- projects_sample_response[0...1].map do |project|
+ projects_sample_response.first(1).map do |project|
project.except(:slug)
end
end
@@ -50,7 +51,7 @@ RSpec.describe ErrorTracking::SentryClient::Projects do
context 'optional keys missing in sentry response' do
let(:sentry_api_response) do
- projects_sample_response[0...1].map do |project|
+ projects_sample_response.first(1).map do |project|
project[:organization].delete(:id)
project.delete(:id)
project.except(:status)
diff --git a/spec/lib/error_tracking/sentry_client/repo_spec.rb b/spec/lib/error_tracking/sentry_client/repo_spec.rb
index 9a1c7a69c3d..445a8e35f8e 100644
--- a/spec/lib/error_tracking/sentry_client/repo_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/repo_spec.rb
@@ -19,12 +19,13 @@ RSpec.describe ErrorTracking::SentryClient::Repo do
subject { client.repos(organization_slug) }
it_behaves_like 'calls sentry api'
+ it_behaves_like 'Sentry API response size limit'
it { is_expected.to all( be_a(Gitlab::ErrorTracking::Repo)) }
it { expect(subject.length).to eq(1) }
- context 'redirects' do
+ context 'with redirects' do
let(:sentry_api_url) { sentry_repos_url }
it_behaves_like 'no Sentry redirects'
diff --git a/spec/lib/error_tracking/sentry_client_spec.rb b/spec/lib/error_tracking/sentry_client_spec.rb
index 9ffd756f057..633b7ae9a91 100644
--- a/spec/lib/error_tracking/sentry_client_spec.rb
+++ b/spec/lib/error_tracking/sentry_client_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe ErrorTracking::SentryClient do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb
index ad2a3c7b462..3e8d71ac673 100644
--- a/spec/lib/gitlab/alert_management/payload/base_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb
@@ -347,4 +347,26 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
it { is_expected.to be(true) }
end
+
+ describe '#source' do
+ subject { parsed_payload.source }
+
+ it { is_expected.to be_nil }
+
+ context 'with alerting integration provided' do
+ before do
+ parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION')
+ end
+
+ it { is_expected.to eq('INTEGRATION') }
+ end
+
+ context 'with monitoring tool defined in the raw payload' do
+ before do
+ allow(parsed_payload).to receive(:monitoring_tool).and_return('TOOL')
+ end
+
+ it { is_expected.to eq('TOOL') }
+ end
+ end
end
diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
index 59933f7459d..bc3b6edc638 100644
--- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
@@ -144,4 +144,40 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do
it { is_expected.to eq(value) }
end
end
+
+ describe '#resolved?' do
+ subject { parsed_payload.resolved? }
+
+ context 'without end time' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with end time' do
+ let(:raw_payload) { { 'end_time' => Time.current.to_s } }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe '#source' do
+ subject { parsed_payload.source }
+
+ it { is_expected.to eq('Generic Alert Endpoint') }
+
+ context 'with alerting integration provided' do
+ before do
+ parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION')
+ end
+
+ it { is_expected.to eq('INTEGRATION') }
+ end
+
+ context 'with monitoring tool defined in the raw payload' do
+ before do
+ allow(parsed_payload).to receive(:monitoring_tool).and_return('TOOL')
+ end
+
+ it { is_expected.to eq('TOOL') }
+ end
+ end
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
index 6fc658ecade..1e0034e386e 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do
let(:instance) { described_class.new({}) }
diff --git a/spec/lib/gitlab/analytics/date_filler_spec.rb b/spec/lib/gitlab/analytics/date_filler_spec.rb
new file mode 100644
index 00000000000..3f547f667f2
--- /dev/null
+++ b/spec/lib/gitlab/analytics/date_filler_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Analytics::DateFiller do
+ let(:default_value) { 0 }
+ let(:formatter) { Gitlab::Analytics::DateFiller::DEFAULT_DATE_FORMATTER }
+
+ subject(:filler_result) do
+ described_class.new(data,
+ from: from,
+ to: to,
+ period: period,
+ default_value: default_value,
+ date_formatter: formatter).fill.to_a
+ end
+
+ context 'when unknown period is given' do
+ it 'raises error' do
+ input = { 3.days.ago.to_date => 10, Date.today => 5 }
+
+ expect do
+ described_class.new(input, from: 4.days.ago, to: Date.today, period: :unknown).fill
+ end.to raise_error(/Unknown period given/)
+ end
+ end
+
+ context 'when period=:day' do
+ let(:from) { Date.new(2021, 5, 25) }
+ let(:to) { Date.new(2021, 6, 5) }
+ let(:period) { :day }
+
+ let(:expected_result) do
+ {
+ Date.new(2021, 5, 25) => 1,
+ Date.new(2021, 5, 26) => default_value,
+ Date.new(2021, 5, 27) => default_value,
+ Date.new(2021, 5, 28) => default_value,
+ Date.new(2021, 5, 29) => default_value,
+ Date.new(2021, 5, 30) => default_value,
+ Date.new(2021, 5, 31) => default_value,
+ Date.new(2021, 6, 1) => default_value,
+ Date.new(2021, 6, 2) => default_value,
+ Date.new(2021, 6, 3) => 10,
+ Date.new(2021, 6, 4) => default_value,
+ Date.new(2021, 6, 5) => default_value
+ }
+ end
+
+ let(:data) do
+ {
+ Date.new(2021, 6, 3) => 10, # deliberatly not sorted
+ Date.new(2021, 5, 27) => nil,
+ Date.new(2021, 5, 25) => 1
+ }
+ end
+
+ it { is_expected.to eq(expected_result.to_a) }
+
+ context 'when a custom default value is given' do
+ let(:default_value) { 'MISSING' }
+
+ it do
+ is_expected.to eq(expected_result.to_a)
+ end
+ end
+
+ context 'when a custom date formatter is given' do
+ let(:formatter) { -> (date) { date.to_s } }
+
+ it do
+ expected_result.transform_keys!(&:to_s)
+
+ is_expected.to eq(expected_result.to_a)
+ end
+ end
+
+ context 'when the data contains dates outside of the requested period' do
+ before do
+ data[Date.new(2022, 6, 1)] = 5
+ end
+
+ it 'raises error' do
+ expect { filler_result }.to raise_error(/Input contains values which doesn't/)
+ end
+ end
+ end
+
+ context 'when period=:week' do
+ let(:from) { Date.new(2021, 5, 16) }
+ let(:to) { Date.new(2021, 6, 7) }
+ let(:period) { :week }
+ let(:data) do
+ {
+ Date.new(2021, 5, 24) => nil,
+ Date.new(2021, 6, 7) => 10
+ }
+ end
+
+ let(:expected_result) do
+ {
+ Date.new(2021, 5, 10) => 0,
+ Date.new(2021, 5, 17) => 0,
+ Date.new(2021, 5, 24) => 0,
+ Date.new(2021, 5, 31) => 0,
+ Date.new(2021, 6, 7) => 10
+ }
+ end
+
+ it do
+ is_expected.to eq(expected_result.to_a)
+ end
+ end
+
+ context 'when period=:month' do
+ let(:from) { Date.new(2021, 5, 1) }
+ let(:to) { Date.new(2021, 7, 1) }
+ let(:period) { :month }
+ let(:data) do
+ {
+ Date.new(2021, 5, 1) => 100
+ }
+ end
+
+ let(:expected_result) do
+ {
+ Date.new(2021, 5, 1) => 100,
+ Date.new(2021, 6, 1) => 0,
+ Date.new(2021, 7, 1) => 0
+ }
+ end
+
+ it do
+ is_expected.to eq(expected_result.to_a)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb b/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb
index b34ac538b24..12679b51ce9 100644
--- a/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb
+++ b/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::ApplicationRateLimiter::BaseStrategy do
describe '#increment' do
diff --git a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
index 84c2cda496e..de1b3e2af71 100644
--- a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
+++ b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Asciidoc::Html5Converter do
describe 'convert AsciiDoc to HTML5' do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index b2bce2076b0..8fec8bce23e 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -638,9 +638,9 @@ module Gitlab
context 'with project' do
let(:context) do
{
- commit: commit,
- project: project,
- ref: ref,
+ commit: commit,
+ project: project,
+ ref: ref,
requested_path: requested_path
}
end
diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb
index fc5917ca583..f743515e616 100644
--- a/spec/lib/gitlab/audit/auditor_spec.rb
+++ b/spec/lib/gitlab/audit/auditor_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Audit::Auditor do
let(:name) { 'audit_operation' }
- let(:author) { create(:user) }
+ let(:author) { create(:user, :with_sign_ins) }
let(:group) { create(:group) }
let(:provider) { 'standard' }
let(:context) do
@@ -37,6 +37,13 @@ RSpec.describe Gitlab::Audit::Auditor do
).and_call_original
audit!
+
+ authentication_event = AuthenticationEvent.last
+
+ expect(authentication_event.user).to eq(author)
+ expect(authentication_event.user_name).to eq(author.name)
+ expect(authentication_event.ip_address).to eq(author.current_sign_in_ip)
+ expect(authentication_event.provider).to eq(provider)
end
it 'logs audit events to database', :aggregate_failures do
diff --git a/spec/lib/gitlab/audit/null_target_spec.rb b/spec/lib/gitlab/audit/null_target_spec.rb
index f192e0cd8db..9197b72afd0 100644
--- a/spec/lib/gitlab/audit/null_target_spec.rb
+++ b/spec/lib/gitlab/audit/null_target_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Audit::NullTarget do
subject { described_class.new }
diff --git a/spec/lib/gitlab/audit/type/definition_spec.rb b/spec/lib/gitlab/audit/type/definition_spec.rb
new file mode 100644
index 00000000000..9f4282a4ec0
--- /dev/null
+++ b/spec/lib/gitlab/audit/type/definition_spec.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Audit::Type::Definition do
+ let(:attributes) do
+ { name: 'group_deploy_token_destroyed',
+ description: 'Group deploy token is deleted',
+ introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/1',
+ introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1',
+ group: 'govern::compliance',
+ milestone: '15.4',
+ saved_to_database: true,
+ streamed: true }
+ end
+
+ let(:path) { File.join('types', 'group_deploy_token_destroyed.yml') }
+ let(:definition) { described_class.new(path, attributes) }
+ let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
+
+ describe '#key' do
+ subject { definition.key }
+
+ it 'returns a symbol from name' do
+ is_expected.to eq(:group_deploy_token_destroyed)
+ end
+ end
+
+ describe '#validate!', :aggregate_failures do
+ using RSpec::Parameterized::TableSyntax
+
+ # rubocop:disable Layout/LineLength
+ where(:param, :value, :result) do
+ :path | 'audit_event/types/invalid.yml' | /Audit event type 'group_deploy_token_destroyed' has an invalid path/
+ :name | nil | %r{property '/name' is not of type: string}
+ :description | nil | %r{property '/description' is not of type: string}
+ :introduced_by_issue | nil | %r{property '/introduced_by_issue' is not of type: string}
+ :introduced_by_mr | nil | %r{property '/introduced_by_mr' is not of type: string}
+ :group | nil | %r{property '/group' is not of type: string}
+ :milestone | nil | %r{property '/milestone' is not of type: string}
+ end
+ # rubocop:enable Layout/LineLength
+
+ with_them do
+ let(:params) { attributes.merge(path: path) }
+
+ before do
+ params[param] = value
+ end
+
+ it do
+ expect do
+ described_class.new(
+ params[:path], params.except(:path)
+ ).validate!
+ end.to raise_error(result)
+ end
+ end
+
+ context 'when both saved_to_database and streamed are false' do
+ let(:params) { attributes.merge({ path: path, saved_to_database: false, streamed: false }) }
+
+ it 'raises an exception' do
+ expect do
+ described_class.new(
+ params[:path], params.except(:path)
+ ).validate!
+ end.to raise_error(/root is invalid: error_type=not/)
+ end
+ end
+ end
+
+ describe '.paths' do
+ it 'returns at least one path' do
+ expect(described_class.paths).not_to be_empty
+ end
+ end
+
+ describe '.get' do
+ before do
+ allow(described_class).to receive(:definitions) do
+ { definition.key => definition }
+ end
+ end
+
+ context 'when audit event type is not defined' do
+ let(:undefined_audit_event_type) { 'undefined_audit_event_type' }
+
+ it 'returns nil' do
+ expect(described_class.get(undefined_audit_event_type)).to be nil
+ end
+ end
+
+ context 'when audit event type is defined' do
+ let(:audit_event_type) { 'group_deploy_token_destroyed' }
+
+ it 'returns an instance of Gitlab::Audit::Type::Definition' do
+ expect(described_class.get(audit_event_type)).to be_an_instance_of(described_class)
+ end
+
+ it 'returns the properties as defined for that audit event type', :aggregate_failures do
+ audit_event_type_definition = described_class.get(audit_event_type)
+
+ expect(audit_event_type_definition.name).to eq "group_deploy_token_destroyed"
+ expect(audit_event_type_definition.description).to eq "Group deploy token is deleted"
+ expect(audit_event_type_definition.group).to eq "govern::compliance"
+ expect(audit_event_type_definition.milestone).to eq "15.4"
+ expect(audit_event_type_definition.saved_to_database).to be true
+ expect(audit_event_type_definition.streamed).to be true
+ end
+ end
+ end
+
+ describe '.load_from_file' do
+ it 'properly loads a definition from file' do
+ expect_file_read(path, content: yaml_content)
+
+ expect(described_class.send(:load_from_file, path).attributes)
+ .to eq(definition.attributes)
+ end
+
+ context 'for missing file' do
+ let(:path) { 'missing/audit_events/type/file.yml' }
+
+ it 'raises exception' do
+ expect do
+ described_class.send(:load_from_file, path)
+ end.to raise_error(/Invalid definition for/)
+ end
+ end
+
+ context 'for invalid definition' do
+ it 'raises exception' do
+ expect_file_read(path, content: '{}')
+
+ expect do
+ described_class.send(:load_from_file, path)
+ end.to raise_error(%r{property '/name' is not of type: string})
+ end
+ end
+ end
+
+ describe '.load_all!' do
+ let(:store1) { Dir.mktmpdir('path1') }
+ let(:store2) { Dir.mktmpdir('path2') }
+ let(:definitions) { {} }
+
+ before do
+ allow(described_class).to receive(:paths).and_return(
+ [
+ File.join(store1, '**', '*.yml'),
+ File.join(store2, '**', '*.yml')
+ ]
+ )
+ end
+
+ subject { described_class.send(:load_all!) }
+
+ after do
+ FileUtils.rm_rf(store1)
+ FileUtils.rm_rf(store2)
+ end
+
+ it "when there are no audit event types a list of definitions is empty" do
+ is_expected.to be_empty
+ end
+
+ it "when there's a single audit event type it properly loads them" do
+ write_audit_event_type(store1, path, yaml_content)
+
+ is_expected.to be_one
+ end
+
+ it "when the same audit event type is stored multiple times raises exception" do
+ write_audit_event_type(store1, path, yaml_content)
+ write_audit_event_type(store2, path, yaml_content)
+
+ expect { subject }
+ .to raise_error(/Audit event type 'group_deploy_token_destroyed' is already defined/)
+ end
+
+ it "when one of the YAMLs is invalid it does raise exception" do
+ write_audit_event_type(store1, path, '{}')
+
+ expect { subject }.to raise_error(/Invalid definition for .* '' must match the filename/)
+ end
+ end
+
+ describe '.definitions' do
+ let(:store1) { Dir.mktmpdir('path1') }
+
+ before do
+ allow(described_class).to receive(:paths).and_return(
+ [
+ File.join(store1, '**', '*.yml')
+ ]
+ )
+ end
+
+ subject { described_class.definitions }
+
+ after do
+ FileUtils.rm_rf(store1)
+ end
+
+ it "loads the definitions for all the audit event types" do
+ write_audit_event_type(store1, path, yaml_content)
+
+ is_expected.to be_one
+ end
+ end
+
+ def write_audit_event_type(store, path, content)
+ path = File.join(store, path)
+ dir = File.dirname(path)
+ FileUtils.mkdir_p(dir)
+ File.write(path, content)
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
index 9dff7f7b3dc..e8008aeaf57 100644
--- a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
@@ -20,17 +20,17 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do
let(:info) do
{
- name: 'Smith, J.',
- email: 'johnsmith@example.com',
+ name: 'Smith, J.',
+ email: 'johnsmith@example.com',
nickname: '123456'
}
end
let(:raw_info) do
{
- uid: ['123456'],
- email: ['johnsmith@example.com'],
- cn: ['Smith, J.'],
+ uid: ['123456'],
+ email: ['johnsmith@example.com'],
+ cn: ['Smith, J.'],
fullName: ['John Smith']
}
end
@@ -52,8 +52,8 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do
let(:attributes) do
{
- 'username' => %w(mail email),
- 'name' => 'fullName'
+ 'username' => %w(mail email),
+ 'name' => 'fullName'
}
end
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 3039fce6141..3be983857bc 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -112,8 +112,8 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'constructs basic options' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 386,
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
'encryption' => 'plain'
}
)
@@ -129,16 +129,16 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'includes failover hosts when set' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'hosts' => [
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'hosts' => [
['ldap1.example.com', 636],
['ldap2.example.com', 636]
],
- 'encryption' => 'simple_tls',
+ 'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'bind_dn' => 'uid=admin,dc=example,dc=com',
- 'password' => 'super_secret'
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
}
)
@@ -158,12 +158,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'includes authentication options when auth is configured' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'bind_dn' => 'uid=admin,dc=example,dc=com',
- 'password' => 'super_secret'
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
}
)
@@ -179,9 +179,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'sets encryption method to simple_tls when configured as simple_tls' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls'
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls'
}
)
@@ -191,9 +191,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'sets encryption method to start_tls when configured as start_tls' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'start_tls'
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls'
}
)
@@ -203,12 +203,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'transforms SSL cert and key to OpenSSL objects' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'start_tls',
- 'tls_options' => {
- 'cert' => raw_cert,
- 'key' => raw_key
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls',
+ 'tls_options' => {
+ 'cert' => raw_cert,
+ 'key' => raw_key
}
}
)
@@ -221,12 +221,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
allow(Gitlab::AppLogger).to receive(:error)
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'start_tls',
- 'tls_options' => {
- 'cert' => 'invalid cert',
- 'key' => 'invalid_key'
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls',
+ 'tls_options' => {
+ 'cert' => 'invalid cert',
+ 'key' => 'invalid_key'
}
}
)
@@ -240,9 +240,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'sets tls_options to OpenSSL defaults' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true
}
)
@@ -255,9 +255,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'sets verify_mode to OpenSSL VERIFY_NONE' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => false
}
)
@@ -274,11 +274,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'passes it through in tls_options' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
- 'tls_options' => {
- 'ca_file' => '/etc/ca.pem'
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'tls_options' => {
+ 'ca_file' => '/etc/ca.pem'
}
}
)
@@ -291,11 +291,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'does not add the ca_file key to tls_options' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
- 'tls_options' => {
- 'ca_file' => ' '
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'tls_options' => {
+ 'ca_file' => ' '
}
}
)
@@ -308,11 +308,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'passes it through in tls_options' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
- 'tls_options' => {
- 'ssl_version' => 'TLSv1_2'
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'tls_options' => {
+ 'ssl_version' => 'TLSv1_2'
}
}
)
@@ -325,11 +325,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'does not add the ssl_version key to tls_options' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
- 'tls_options' => {
- 'ssl_version' => ' '
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'tls_options' => {
+ 'ssl_version' => ' '
}
}
)
@@ -343,11 +343,11 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'constructs basic options' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 386,
- 'base' => 'ou=users,dc=example,dc=com',
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'base' => 'ou=users,dc=example,dc=com',
'encryption' => 'plain',
- 'uid' => 'uid'
+ 'uid' => 'uid'
}
)
@@ -364,10 +364,10 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'includes authentication options when auth is configured' do
stub_ldap_config(
options: {
- 'uid' => 'sAMAccountName',
+ 'uid' => 'sAMAccountName',
'user_filter' => '(memberOf=cn=group1,ou=groups,dc=example,dc=com)',
- 'bind_dn' => 'uid=admin,dc=example,dc=com',
- 'password' => 'super_secret'
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
}
)
@@ -381,12 +381,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'transforms SSL cert and key to OpenSSL objects' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'start_tls',
- 'tls_options' => {
- 'cert' => raw_cert,
- 'key' => raw_key
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls',
+ 'tls_options' => {
+ 'cert' => raw_cert,
+ 'key' => raw_key
}
}
)
@@ -399,9 +399,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'specifies disable_verify_certificates as false' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true
}
)
@@ -414,9 +414,9 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'specifies disable_verify_certificates as true' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => false
}
)
@@ -429,12 +429,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'passes it through' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'tls_options' => {
- 'ca_file' => '/etc/ca.pem'
+ 'tls_options' => {
+ 'ca_file' => '/etc/ca.pem'
}
}
)
@@ -447,12 +447,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'does not include the ca_file option' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'tls_options' => {
- 'ca_file' => ' '
+ 'tls_options' => {
+ 'ca_file' => ' '
}
}
)
@@ -465,12 +465,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'passes it through' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'tls_options' => {
- 'ssl_version' => 'TLSv1_2'
+ 'tls_options' => {
+ 'ssl_version' => 'TLSv1_2'
}
}
)
@@ -483,12 +483,12 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'does not include the ssl_version option' do
stub_ldap_config(
options: {
- 'host' => 'ldap.example.com',
- 'port' => 686,
- 'encryption' => 'simple_tls',
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'tls_options' => {
- 'ssl_version' => ' '
+ 'tls_options' => {
+ 'ssl_version' => ' '
}
}
)
@@ -503,7 +503,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'is true when password is set' do
stub_ldap_config(
options: {
- 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
'password' => 'super_secret'
}
)
@@ -514,7 +514,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
it 'is true when bind_dn is set and password is empty' do
stub_ldap_config(
options: {
- 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
'password' => ''
}
)
@@ -539,15 +539,15 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
options: {
'attributes' => {
'username' => %w(sAMAccountName),
- 'email' => %w(userPrincipalName)
+ 'email' => %w(userPrincipalName)
}
}
)
expect(config.attributes).to include({
'username' => %w(sAMAccountName),
- 'email' => %w(userPrincipalName),
- 'name' => 'cn'
+ 'email' => %w(userPrincipalName),
+ 'name' => 'cn'
})
end
end
diff --git a/spec/lib/gitlab/auth/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb
index 6857b561370..f8268bb1666 100644
--- a/spec/lib/gitlab/auth/ldap/person_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/person_spec.rb
@@ -10,10 +10,10 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
before do
stub_ldap_config(
options: {
- 'uid' => 'uid',
+ 'uid' => 'uid',
'attributes' => {
- 'name' => 'cn',
- 'email' => %w(mail email userPrincipalName),
+ 'name' => 'cn',
+ 'email' => %w(mail email userPrincipalName),
'username' => username_attribute
}
}
@@ -53,10 +53,10 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
it 'returns a compact and unique array' do
stub_ldap_config(
options: {
- 'uid' => nil,
+ 'uid' => nil,
'attributes' => {
- 'name' => 'cn',
- 'email' => 'mail',
+ 'name' => 'cn',
+ 'email' => 'mail',
'username' => %w(uid mail),
'first_name' => ''
}
diff --git a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
index a044094179c..c94f962ee93 100644
--- a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
@@ -40,12 +40,12 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash do
let(:info_hash) do
{
- email: email_ascii,
+ email: email_ascii,
first_name: first_name_ascii,
- last_name: last_name_ascii,
- name: name_ascii,
- nickname: nickname_ascii,
- uid: uid_ascii,
+ last_name: last_name_ascii,
+ name: name_ascii,
+ nickname: nickname_ascii,
+ uid: uid_ascii,
address: {
locality: 'some locality',
country: 'some country'
diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
index c1b96819176..96a31c50989 100644
--- a/spec/lib/gitlab/auth/o_auth/provider_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
@@ -90,6 +90,24 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
end
end
end
+
+ context 'for an OpenID Connect provider' do
+ before do
+ provider = ActiveSupport::InheritableOptions.new(
+ name: 'openid_connect',
+ args: ActiveSupport::InheritableOptions.new(name: 'custom_oidc')
+ )
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
+ end
+
+ context 'when the provider exists' do
+ subject { described_class.config_for('custom_oidc') }
+
+ it 'returns the config' do
+ expect(subject).to be_a(ActiveSupport::InheritableOptions)
+ end
+ end
+ end
end
describe '.label_for' do
diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
index 57ee53a452e..61e17ad2424 100644
--- a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
+++ b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
@@ -49,10 +49,10 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
stub_request(:post, otp_verification_url)
.with(body: JSON(otp_verification_request_body),
- headers: {
- 'Content-Type' => 'application/json',
- 'Authorization' => "Bearer #{access_token}"
- })
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{access_token}"
+ })
.to_return(status: otp_verification_response_status, body: '', headers: {})
end
diff --git a/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb
new file mode 100644
index 00000000000..3aab0cdf54b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilities, :migration do # rubocop:disable Layout/LineLength
+ let(:migration) do
+ described_class.new(start_id: 1, end_id: 10,
+ batch_table: table_name, batch_column: batch_column,
+ sub_batch_size: sub_batch_size, pause_ms: pause_ms,
+ connection: ApplicationRecord.connection)
+ end
+
+ let(:users_table) { table(:users) }
+ let(:vulnerability_reads_table) { table(:vulnerability_reads) }
+ let(:vulnerability_scanners_table) { table(:vulnerability_scanners) }
+ let(:vulnerabilities_table) { table(:vulnerabilities) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:cluster_agents_table) { table(:cluster_agents) }
+
+ let(:table_name) { 'cluster_agents' }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 100 }
+ let(:pause_ms) { 0 }
+
+ subject(:perform_migration) { migration.perform }
+
+ before do
+ users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5)
+
+ namespaces_table.create!(id: 1, name: 'Namespace 1', path: 'namespace-1')
+ namespaces_table.create!(id: 2, name: 'Namespace 2', path: 'namespace-2')
+ namespaces_table.create!(id: 3, name: 'Namespace 3', path: 'namespace-3')
+
+ projects_table.create!(id: 1, namespace_id: 1, name: 'Project 1', path: 'project-1', project_namespace_id: 1)
+ projects_table.create!(id: 2, namespace_id: 2, name: 'Project 2', path: 'project-2', project_namespace_id: 2)
+ projects_table.create!(id: 3, namespace_id: 2, name: 'Project 3', path: 'project-3', project_namespace_id: 3)
+
+ cluster_agents_table.create!(id: 1, name: 'Agent 1', project_id: 1)
+ cluster_agents_table.create!(id: 2, name: 'Agent 2', project_id: 2)
+ cluster_agents_table.create!(id: 3, name: 'Agent 3', project_id: 1)
+ cluster_agents_table.create!(id: 4, name: 'Agent 4', project_id: 1)
+ cluster_agents_table.create!(id: 5, name: 'Agent 5', project_id: 1)
+ cluster_agents_table.create!(id: 6, name: 'Agent 6', project_id: 1)
+ cluster_agents_table.create!(id: 7, name: 'Agent 7', project_id: 3)
+ cluster_agents_table.create!(id: 8, name: 'Agent 8', project_id: 1)
+ cluster_agents_table.create!(id: 9, name: 'Agent 9', project_id: 1)
+ cluster_agents_table.create!(id: 10, name: 'Agent 10', project_id: 3)
+ cluster_agents_table.create!(id: 11, name: 'Agent 11', project_id: 1)
+
+ vulnerability_scanners_table.create!(id: 1, project_id: 1, external_id: 'starboard', name: 'Starboard')
+ vulnerability_scanners_table.create!(id: 2, project_id: 2, external_id: 'starboard', name: 'Starboard')
+ vulnerability_scanners_table.create!(id: 3, project_id: 3, external_id: 'starboard', name: 'Starboard')
+
+ add_vulnerability_read!(1, project_id: 1, cluster_agent_id: 1, report_type: 7)
+ add_vulnerability_read!(2, project_id: 1, cluster_agent_id: nil, report_type: 7)
+ add_vulnerability_read!(3, project_id: 1, cluster_agent_id: 3, report_type: 7)
+ add_vulnerability_read!(4, project_id: 1, cluster_agent_id: nil, report_type: 7)
+ add_vulnerability_read!(5, project_id: 2, cluster_agent_id: 5, report_type: 5)
+ add_vulnerability_read!(7, project_id: 2, cluster_agent_id: 7, report_type: 7)
+ add_vulnerability_read!(9, project_id: 3, cluster_agent_id: 9, report_type: 7)
+ add_vulnerability_read!(10, project_id: 1, cluster_agent_id: 10, report_type: 7)
+ add_vulnerability_read!(11, project_id: 2, cluster_agent_id: 11, report_type: 7)
+ end
+
+ it 'backfills `has_vulnerabilities` for the selected records', :aggregate_failures do
+ queries = ActiveRecord::QueryRecorder.new do
+ perform_migration
+ end
+
+ expect(queries.count).to eq(3)
+ expect(cluster_agents_table.where(has_vulnerabilities: true).count).to eq 2
+ expect(cluster_agents_table.where(has_vulnerabilities: true).pluck(:id)).to match_array([1, 3])
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+
+ private
+
+ def add_vulnerability_read!(id, project_id:, cluster_agent_id:, report_type:)
+ vulnerabilities_table.create!(
+ id: id,
+ project_id: project_id,
+ author_id: 1,
+ title: "Vulnerability #{id}",
+ severity: 5,
+ confidence: 5,
+ report_type: report_type
+ )
+
+ vulnerability_reads_table.create!(
+ id: id,
+ uuid: SecureRandom.uuid,
+ severity: 5,
+ state: 1,
+ vulnerability_id: id,
+ scanner_id: project_id,
+ casted_cluster_agent_id: cluster_agent_id,
+ project_id: project_id,
+ report_type: report_type
+ )
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
index e363a5a6b20..8947262ae9e 100644
--- a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
@@ -14,10 +14,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillImportedIssueSearchData,
table(:projects)
.create!(
namespace_id: namespace.id,
- creator_id: user.id,
- name: 'projecty',
- path: 'path',
- project_namespace_id: namespace.id)
+ creator_id: user.id,
+ name: 'projecty',
+ path: 'path',
+ project_namespace_id: namespace.id)
end
let!(:issue) do
diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb
index b3825a7c4ea..3c0b7766871 100644
--- a/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb
@@ -9,25 +9,35 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsEnableSslVerific
before do
integrations.create!(id: 1, type_new: 'Integrations::Bamboo') # unaffected integration
integrations.create!(id: 2, type_new: 'Integrations::DroneCi') # no properties
- integrations.create!(id: 3, type_new: 'Integrations::DroneCi',
+ integrations.create!(
+ id: 3, type_new: 'Integrations::DroneCi',
properties: {}) # no URL
- integrations.create!(id: 4, type_new: 'Integrations::DroneCi',
+ integrations.create!(
+ id: 4, type_new: 'Integrations::DroneCi',
properties: { 'drone_url' => '' }) # blank URL
- integrations.create!(id: 5, type_new: 'Integrations::DroneCi',
+ integrations.create!(
+ id: 5, type_new: 'Integrations::DroneCi',
properties: { 'drone_url' => 'https://example.com:foo' }) # invalid URL
- integrations.create!(id: 6, type_new: 'Integrations::DroneCi',
+ integrations.create!(
+ id: 6, type_new: 'Integrations::DroneCi',
properties: { 'drone_url' => 'https://example.com' }) # unknown URL
- integrations.create!(id: 7, type_new: 'Integrations::DroneCi',
+ integrations.create!(
+ id: 7, type_new: 'Integrations::DroneCi',
properties: { 'drone_url' => 'http://cloud.drone.io' }) # no HTTPS
- integrations.create!(id: 8, type_new: 'Integrations::DroneCi',
+ integrations.create!(
+ id: 8, type_new: 'Integrations::DroneCi',
properties: { 'drone_url' => 'https://cloud.drone.io' }) # known URL
- integrations.create!(id: 9, type_new: 'Integrations::Teamcity',
+ integrations.create!(
+ id: 9, type_new: 'Integrations::Teamcity',
properties: { 'teamcity_url' => 'https://example.com' }) # unknown URL
- integrations.create!(id: 10, type_new: 'Integrations::Teamcity',
+ integrations.create!(
+ id: 10, type_new: 'Integrations::Teamcity',
properties: { 'teamcity_url' => 'https://foo.bar.teamcity.com' }) # unknown URL
- integrations.create!(id: 11, type_new: 'Integrations::Teamcity',
+ integrations.create!(
+ id: 11, type_new: 'Integrations::Teamcity',
properties: { 'teamcity_url' => 'https://teamcity.com' }) # unknown URL
- integrations.create!(id: 12, type_new: 'Integrations::Teamcity',
+ integrations.create!(
+ id: 12, type_new: 'Integrations::Teamcity',
properties: { 'teamcity_url' => 'https://customer.teamcity.com' }) # known URL
end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
new file mode 100644
index 00000000000..29833074109
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+# todo: this will need to specify schema version once we introduce the not null constraint on issues#namespace_id
+# https://gitlab.com/gitlab-org/gitlab/-/issues/367835
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+
+ let(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') }
+ let(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'space2') }
+
+ let(:proj_namespace1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id) }
+ let(:proj_namespace2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace2.id) }
+
+ # rubocop:disable Layout/LineLength
+ let(:proj1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id) }
+ let(:proj2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: namespace2.id, project_namespace_id: proj_namespace2.id) }
+
+ let!(:proj1_issue_with_namespace) { issues.create!(title: 'issue1', project_id: proj1.id, namespace_id: proj_namespace1.id) }
+ let!(:proj1_issue_without_namespace1) { issues.create!(title: 'issue2', project_id: proj1.id) }
+ let!(:proj1_issue_without_namespace2) { issues.create!(title: 'issue3', project_id: proj1.id) }
+ let!(:proj2_issue_with_namespace) { issues.create!(title: 'issue4', project_id: proj2.id, namespace_id: proj_namespace2.id) }
+ let!(:proj2_issue_without_namespace1) { issues.create!(title: 'issue5', project_id: proj2.id) }
+ let!(:proj2_issue_without_namespace2) { issues.create!(title: 'issue6', project_id: proj2.id) }
+ # rubocop:enable Layout/LineLength
+
+ let(:migration) do
+ described_class.new(
+ start_id: proj1_issue_with_namespace.id,
+ end_id: proj2_issue_without_namespace2.id,
+ batch_table: :issues,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 2,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ it 'backfills namespace_id for the selected records', :aggregate_failures do
+ perform_migration
+
+ expected_namespaces = [proj_namespace1.id, proj_namespace2.id]
+
+ expect(issues.where.not(namespace_id: nil).count).to eq(6)
+ expect(issues.where.not(namespace_id: nil).pluck(:namespace_id).uniq).to match_array(expected_namespaces)
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index 6f75d3faef3..1c2e0e991d9 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -14,23 +14,23 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
let!(:user) do
users.create!(id: 1,
- email: 'user@example.com',
- projects_limit: 10,
- username: 'test',
- name: user_name,
- state: user_state,
- last_activity_on: 1.minute.ago,
- user_type: user_type,
- confirmed_at: 1.day.ago)
+ email: 'user@example.com',
+ projects_limit: 10,
+ username: 'test',
+ name: user_name,
+ state: user_state,
+ last_activity_on: 1.minute.ago,
+ user_type: user_type,
+ confirmed_at: 1.day.ago)
end
let!(:migration_bot) do
users.create!(id: 100,
- email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}",
- user_type: HasUserType::USER_TYPES[:migration_bot],
- name: 'GitLab Migration Bot',
- projects_limit: 10,
- username: 'bot')
+ email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}",
+ user_type: HasUserType::USER_TYPES[:migration_bot],
+ name: 'GitLab Migration Bot',
+ projects_limit: 10,
+ username: 'bot')
end
let!(:snippet_with_repo) { snippets.create!(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
@@ -260,14 +260,14 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
let(:user_name) { '.' }
let!(:other_user) do
users.create!(id: 2,
- email: 'user2@example.com',
- projects_limit: 10,
- username: 'test2',
- name: 'Test2',
- state: user_state,
- last_activity_on: 1.minute.ago,
- user_type: user_type,
- confirmed_at: 1.day.ago)
+ email: 'user2@example.com',
+ projects_limit: 10,
+ username: 'test2',
+ name: 'Test2',
+ state: user_state,
+ last_activity_on: 1.minute.ago,
+ user_type: user_type,
+ confirmed_at: 1.day.ago)
end
let!(:invalid_snippet) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) }
diff --git a/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb b/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb
index 79699375a8d..f642ec8c20d 100644
--- a/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb
@@ -23,8 +23,6 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityReadsClusterAge
let(:sub_batch_size) { 1_000 }
let(:pause_ms) { 0 }
- subject(:perform_migration) { migration.perform }
-
before do
users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5)
@@ -49,20 +47,30 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityReadsClusterAge
add_vulnerability_read!(11, project_id: 1, cluster_agent_id: 1, report_type: 7)
end
- it 'backfills `casted_cluster_agent_id` for the selected records', :aggregate_failures do
- queries = ActiveRecord::QueryRecorder.new do
- perform_migration
+ describe '#filter_batch' do
+ it 'pick only vulnerability reads where report_type = 7' do
+ expect(migration.filter_batch(vulnerability_reads_table).pluck(:id)).to contain_exactly(1, 3, 7, 9, 10, 11)
end
-
- expect(queries.count).to eq(3)
- expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).count).to eq 3
- expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).pluck(:id)).to match_array([1, 9, 10])
end
- it 'tracks timings of queries' do
- expect(migration.batch_metrics.timings).to be_empty
+ describe '#perform' do
+ subject(:perform_migration) { migration.perform }
- expect { perform_migration }.to change { migration.batch_metrics.timings }
+ it 'backfills `casted_cluster_agent_id` for the selected records', :aggregate_failures do
+ queries = ActiveRecord::QueryRecorder.new do
+ perform_migration
+ end
+
+ expect(queries.count).to eq(3)
+ expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).count).to eq 3
+ expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).pluck(:id)).to match_array([1, 9, 10])
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
end
private
diff --git a/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb
index 8d82c533d20..6ef474ad7f9 100644
--- a/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb
@@ -2,12 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :migration, schema: 20220326161803 do
- subject(:migrate) { migration.perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms, issue_type_enum[:issue], issue_type.id) }
-
- let(:migration) { described_class.new }
-
- let(:batch_table) { 'issues' }
+RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :migration, schema: 20220825142324 do
let(:batch_column) { 'id' }
let(:sub_batch_size) { 2 }
let(:pause_ms) { 0 }
@@ -15,7 +10,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :mi
# let_it_be can't be used in migration specs because all tables but `work_item_types` are deleted after each spec
let(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
let(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
- let(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let(:project) { table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
let(:issues_table) { table(:issues) }
let(:issue_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_type_enum[:issue]) }
@@ -32,6 +27,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :mi
let(:all_issues) { [issue1, issue2, issue3, incident1, test_case1, requirement1] }
+ let(:migration) do
+ described_class.new(
+ start_id: start_id,
+ end_id: end_id,
+ batch_table: :issues,
+ batch_column: :id,
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ job_arguments: [issue_type_enum[:issue], issue_type.id],
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:migrate) { migration.perform }
+
it 'sets work_item_type_id only for the given type' do
expect(all_issues).to all(have_attributes(work_item_type_id: nil))
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb
deleted file mode 100644
index 3cba99bfe51..00000000000
--- a/spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillIssueWorkItemTypeBatchingStrategy, '#next_batch', schema: 20220326161803 do # rubocop:disable Layout/LineLength
- # let! can't be used in migration specs because all tables but `work_item_types` are deleted after each spec
- let!(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
- let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
- let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
- let!(:issues_table) { table(:issues) }
- let!(:task_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_type_enum[:task]) }
-
- let!(:issue1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) }
- let!(:task1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task]) }
- let!(:issue2) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) }
- let!(:issue3) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) }
- let!(:task2) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task]) }
- let!(:incident1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:incident]) }
- # test_case is EE only, but enum values exist on the FOSS model
- let!(:test_case1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:test_case]) }
-
- let!(:task3) do
- issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task], work_item_type_id: task_type.id)
- end
-
- let!(:task4) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:task]) }
-
- let!(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) }
-
- context 'when issue_type is issue' do
- let(:job_arguments) { [issue_type_enum[:issue], 'irrelevant_work_item_id'] }
-
- context 'when starting on the first batch' do
- it 'returns the bounds of the next batch' do
- batch_bounds = next_batch(issue1.id, 2)
-
- expect(batch_bounds).to match_array([issue1.id, issue2.id])
- end
- end
-
- context 'when additional batches remain' do
- it 'returns the bounds of the next batch' do
- batch_bounds = next_batch(issue2.id, 2)
-
- expect(batch_bounds).to match_array([issue2.id, issue3.id])
- end
- end
-
- context 'when on the final batch' do
- it 'returns the bounds of the next batch' do
- batch_bounds = next_batch(issue3.id, 2)
-
- expect(batch_bounds).to match_array([issue3.id, issue3.id])
- end
- end
-
- context 'when no additional batches remain' do
- it 'returns nil' do
- batch_bounds = next_batch(issue3.id + 1, 1)
-
- expect(batch_bounds).to be_nil
- end
- end
- end
-
- context 'when issue_type is incident' do
- let(:job_arguments) { [issue_type_enum[:incident], 'irrelevant_work_item_id'] }
-
- context 'when starting on the first batch' do
- it 'returns the bounds of the next batch with only one element' do
- batch_bounds = next_batch(incident1.id, 2)
-
- expect(batch_bounds).to match_array([incident1.id, incident1.id])
- end
- end
- end
-
- context 'when issue_type is requirement and there are no matching records' do
- let(:job_arguments) { [issue_type_enum[:requirement], 'irrelevant_work_item_id'] }
-
- context 'when starting on the first batch' do
- it 'returns nil' do
- batch_bounds = next_batch(1, 2)
-
- expect(batch_bounds).to be_nil
- end
- end
- end
-
- context 'when issue_type is task' do
- let(:job_arguments) { [issue_type_enum[:task], 'irrelevant_work_item_id'] }
-
- context 'when starting on the first batch' do
- it 'returns the bounds of the next batch' do
- batch_bounds = next_batch(task1.id, 2)
-
- expect(batch_bounds).to match_array([task1.id, task2.id])
- end
- end
-
- context 'when additional batches remain' do
- it 'returns the bounds of the next batch, does not skip records where FK is already set' do
- batch_bounds = next_batch(task2.id, 2)
-
- expect(batch_bounds).to match_array([task2.id, task3.id])
- end
- end
-
- context 'when on the final batch' do
- it 'returns the bounds of the next batch' do
- batch_bounds = next_batch(task4.id, 2)
-
- expect(batch_bounds).to match_array([task4.id, task4.id])
- end
- end
-
- context 'when no additional batches remain' do
- it 'returns nil' do
- batch_bounds = next_batch(task4.id + 1, 1)
-
- expect(batch_bounds).to be_nil
- end
- end
- end
-
- def next_batch(min_value, batch_size)
- batching_strategy.next_batch(
- :issues,
- :id,
- batch_min_value: min_value,
- batch_size: batch_size,
- job_arguments: job_arguments
- )
- end
-end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb
index 94e9bcf9207..7076e82ea34 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb
@@ -2,137 +2,6 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectStatisticsWithContainerRegistrySizeBatchingStrategy, '#next_batch' do # rubocop:disable Layout/LineLength
- let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) }
- let(:namespace) { table(:namespaces) }
- let(:project) { table(:projects) }
- let(:container_repositories) { table(:container_repositories) }
-
- let!(:group) do
- namespace.create!(
- name: 'namespace1', type: 'Group', path: 'space1'
- )
- end
-
- let!(:proj_namespace1) do
- namespace.create!(
- name: 'proj1', path: 'proj1', type: 'Project', parent_id: group.id
- )
- end
-
- let!(:proj_namespace2) do
- namespace.create!(
- name: 'proj2', path: 'proj2', type: 'Project', parent_id: group.id
- )
- end
-
- let!(:proj_namespace3) do
- namespace.create!(
- name: 'proj3', path: 'proj3', type: 'Project', parent_id: group.id
- )
- end
-
- let!(:proj1) do
- project.create!(
- name: 'proj1', path: 'proj1', namespace_id: group.id, project_namespace_id: proj_namespace1.id
- )
- end
-
- let!(:proj2) do
- project.create!(
- name: 'proj2', path: 'proj2', namespace_id: group.id, project_namespace_id: proj_namespace2.id
- )
- end
-
- let!(:proj3) do
- project.create!(
- name: 'proj3', path: 'proj3', namespace_id: group.id, project_namespace_id: proj_namespace3.id
- )
- end
-
- let!(:con1) do
- container_repositories.create!(
- project_id: proj1.id,
- name: "ContReg_#{proj1.id}:1",
- migration_state: 'import_done',
- created_at: Date.new(2022, 01, 20)
- )
- end
-
- let!(:con2) do
- container_repositories.create!(
- project_id: proj1.id,
- name: "ContReg_#{proj1.id}:2",
- migration_state: 'import_done',
- created_at: Date.new(2022, 01, 20)
- )
- end
-
- let!(:con3) do
- container_repositories.create!(
- project_id: proj2.id,
- name: "ContReg_#{proj2.id}:1",
- migration_state: 'import_done',
- created_at: Date.new(2022, 01, 20)
- )
- end
-
- let!(:con4) do
- container_repositories.create!(
- project_id: proj3.id,
- name: "ContReg_#{proj3.id}:1",
- migration_state: 'default',
- created_at: Date.new(2022, 02, 20)
- )
- end
-
- let!(:con5) do
- container_repositories.create!(
- project_id: proj3.id,
- name: "ContReg_#{proj3.id}:2",
- migration_state: 'default',
- created_at: Date.new(2022, 02, 20)
- )
- end
-
+RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectStatisticsWithContainerRegistrySizeBatchingStrategy do # rubocop:disable Layout/LineLength
it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy }
-
- context 'when starting on the first batch' do
- it 'returns the bounds of the next batch' do
- batch_bounds = batching_strategy.next_batch(
- :container_repositories,
- :project_id,
- batch_min_value: con1.project_id,
- batch_size: 3,
- job_arguments: []
- )
- expect(batch_bounds).to eq([con1.project_id, con4.project_id])
- end
- end
-
- context 'when additional batches remain' do
- it 'returns the bounds of the next batch' do
- batch_bounds = batching_strategy.next_batch(
- :container_repositories,
- :project_id,
- batch_min_value: con3.project_id,
- batch_size: 3,
- job_arguments: []
- )
-
- expect(batch_bounds).to eq([con3.project_id, con5.project_id])
- end
- end
-
- context 'when no additional batches remain' do
- it 'returns nil' do
- batch_bounds = batching_strategy.next_batch(:container_repositories,
- :project_id,
- batch_min_value: con5.project_id + 1,
- batch_size: 1, job_arguments: []
- )
-
- expect(batch_bounds).to be_nil
- end
- end
end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb
index f96c7de50f2..e4bef81e0bd 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb
@@ -3,117 +3,5 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::DismissedVulnerabilitiesStrategy, '#next_batch' do
- let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) }
- let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let(:users) { table(:users) }
- let(:user) { create_user! }
- let(:project) do
- table(:projects).create!(
- namespace_id: namespace.id,
- project_namespace_id: namespace.id,
- packages_enabled: false)
- end
-
- let(:vulnerabilities) { table(:vulnerabilities) }
-
- let!(:vulnerability1) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id,
- dismissed_at: Time.current
- )
- end
-
- let!(:vulnerability2) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id,
- dismissed_at: Time.current
- )
- end
-
- let!(:vulnerability3) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id,
- dismissed_at: Time.current
- )
- end
-
- let!(:vulnerability4) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id,
- dismissed_at: nil
- )
- end
-
it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy }
-
- context 'when starting on the first batch' do
- it 'returns the bounds of the next batch' do
- batch_bounds = batching_strategy.next_batch(
- :vulnerabilities,
- :id,
- batch_min_value: vulnerability1.id,
- batch_size: 2,
- job_arguments: []
- )
- expect(batch_bounds).to eq([vulnerability1.id, vulnerability2.id])
- end
- end
-
- context 'when additional batches remain' do
- it 'returns the bounds of the next batch and skips the records that do not have `dismissed_at` set' do
- batch_bounds = batching_strategy.next_batch(
- :vulnerabilities,
- :id,
- batch_min_value: vulnerability3.id,
- batch_size: 2,
- job_arguments: []
- )
-
- expect(batch_bounds).to eq([vulnerability3.id, vulnerability3.id])
- end
- end
-
- context 'when no additional batches remain' do
- it 'returns nil' do
- batch_bounds = batching_strategy.next_batch(
- :vulnerabilities,
- :id,
- batch_min_value: vulnerability4.id + 1,
- batch_size: 1,
- job_arguments: []
- )
-
- expect(batch_bounds).to be_nil
- end
- end
-
- private
-
- def create_vulnerability!(
- project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0, state: 1, dismissed_at: nil
- )
- vulnerabilities.create!(
- project_id: project_id,
- author_id: author_id,
- title: title,
- severity: severity,
- confidence: confidence,
- report_type: report_type,
- state: state,
- dismissed_at: dismissed_at
- )
- end
-
- def create_user!(name: "Example User", email: "user@example.com", user_type: nil)
- users.create!(
- name: name,
- email: email,
- username: name,
- projects_limit: 10
- )
- end
end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
index 9fdd7bf8adc..37fdd209622 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
@@ -60,26 +60,21 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
expect(batch_bounds).to eq([namespace4.id, namespace4.id])
end
- end
-
- context 'additional filters' do
- let(:strategy_with_filters) do
- Class.new(described_class) do
- def apply_additional_filters(relation, job_arguments:, job_class: nil)
- min_id = job_arguments.first
- relation.where.not(type: 'Project').where('id >= ?', min_id)
+ context 'when scope has a join which makes the column name ambiguous' do
+ let(:job_class) do
+ Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
+ scope_to ->(r) { r.joins('LEFT JOIN users ON users.id = namespaces.owner_id') }
end
end
- end
- let(:batching_strategy) { strategy_with_filters.new(connection: ActiveRecord::Base.connection) }
- let!(:namespace5) { namespaces.create!(name: 'batchtest5', path: 'batch-test5', type: 'Project') }
+ it 'executes the correct query' do
+ expect(job_class).to receive(:generic_instance).and_call_original
- it 'applies additional filters' do
- batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1])
+ batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [], job_class: job_class)
- expect(batch_bounds).to eq([namespace4.id, namespace4.id])
+ expect(batch_bounds).to eq([namespace4.id, namespace4.id])
+ end
end
end
end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb
new file mode 100644
index 00000000000..e296a46ea2f
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::RemoveBackfilledJobArtifactsExpireAtBatchingStrategy do # rubocop:disable Layout/LineLength
+ it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy }
+end
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
new file mode 100644
index 00000000000..76a9ea82c76
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidGroupMembers, :migration, schema: 20220809002011 do
+ # rubocop: disable Layout/LineLength
+ # rubocop: disable RSpec/ScatteredLet
+ let!(:migration_attrs) do
+ {
+ start_id: 1,
+ end_id: 1000,
+ batch_table: :members,
+ batch_column: :id,
+ sub_batch_size: 100,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ }
+ end
+
+ let!(:migration) { described_class.new(**migration_attrs) }
+
+ subject(:perform_migration) { migration.perform }
+
+ let(:users_table) { table(:users) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:members_table) { table(:members) }
+ let(:projects_table) { table(:projects) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+ let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 5) }
+ let(:user3) { users_table.create!(name: 'user3', email: 'user3@example.com', projects_limit: 5) }
+ let(:user4) { users_table.create!(name: 'user4', email: 'user4@example.com', projects_limit: 5) }
+ let(:user5) { users_table.create!(name: 'user5', email: 'user5@example.com', projects_limit: 5) }
+ let(:user6) { users_table.create!(name: 'user6', email: 'user6@example.com', projects_limit: 5) }
+
+ let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') }
+
+ let!(:group2) { namespaces_table.create!(name: 'outstanding group 2', path: 'group-path-2', type: 'Group') }
+
+ # create group member records, a mix of both valid and invalid
+ # project members will have already been filtered out.
+ let!(:group_member1) { create_invalid_group_member(id: 1, user_id: user1.id) }
+
+ let!(:group_member4) { create_valid_group_member(id: 4, user_id: user2.id, group_id: group1.id) }
+
+ let!(:group_member5) { create_valid_group_member(id: 5, user_id: user3.id, group_id: group2.id) }
+
+ let!(:group_member6) { create_invalid_group_member(id: 6, user_id: user4.id) }
+
+ let!(:group_member7) { create_valid_group_member(id: 7, user_id: user5.id, group_id: group1.id) }
+
+ let!(:group_member8) { create_invalid_group_member(id: 8, user_id: user6.id) }
+
+ it 'removes invalid memberships but keeps valid ones', :aggregate_failures do
+ expect(members_table.where(type: 'GroupMember').count).to eq 6
+
+ queries = ActiveRecord::QueryRecorder.new do
+ perform_migration
+ end
+
+ expect(queries.count).to eq(4)
+ expect(members_table.where(type: 'GroupMember').pluck(:id)).to match_array([group_member4, group_member5, group_member7].map(&:id))
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+
+ it 'logs IDs of deleted records' do
+ expect(Gitlab::AppLogger).to receive(:info).with({ message: 'Removing invalid group member records',
+ deleted_count: 3, ids: [group_member1, group_member6, group_member8].map(&:id) })
+
+ perform_migration
+ end
+
+ def create_invalid_group_member(id:, user_id:)
+ members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember", source_type: "Namespace", notification_level: 3, member_namespace_id: nil)
+ end
+
+ def create_valid_group_member(id:, user_id:, group_id:)
+ members_table.create!(id: id, user_id: user_id, source_id: group_id, access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember", source_type: "Namespace", member_namespace_id: group_id, notification_level: 3)
+ end
+ # rubocop: enable Layout/LineLength
+ # rubocop: enable RSpec/ScatteredLet
+end
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
new file mode 100644
index 00000000000..029a6adf831
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migration, schema: 20220901035725 do
+ # rubocop: disable Layout/LineLength
+ # rubocop: disable RSpec/ScatteredLet
+ let!(:migration_attrs) do
+ {
+ start_id: 1,
+ end_id: 1000,
+ batch_table: :members,
+ batch_column: :id,
+ sub_batch_size: 100,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ }
+ end
+
+ let!(:migration) { described_class.new(**migration_attrs) }
+
+ subject(:perform_migration) { migration.perform }
+
+ let(:users_table) { table(:users) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:members_table) { table(:members) }
+ let(:projects_table) { table(:projects) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+ let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 5) }
+ let(:user3) { users_table.create!(name: 'user3', email: 'user3@example.com', projects_limit: 5) }
+ let(:user4) { users_table.create!(name: 'user4', email: 'user4@example.com', projects_limit: 5) }
+ let(:user5) { users_table.create!(name: 'user5', email: 'user5@example.com', projects_limit: 5) }
+ let(:user6) { users_table.create!(name: 'user6', email: 'user6@example.com', projects_limit: 5) }
+
+ let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') }
+
+ let!(:project_namespace1) do
+ namespaces_table.create!(name: 'fabulous project', path: 'project-path-1', type: 'ProjectNamespace',
+ parent_id: group1.id)
+ end
+
+ let!(:project1) do
+ projects_table.create!(name: 'fabulous project', path: 'project-path-1', project_namespace_id: project_namespace1.id,
+ namespace_id: group1.id)
+ end
+
+ let!(:project_namespace2) do
+ namespaces_table.create!(name: 'splendiferous project', path: 'project-path-2', type: 'ProjectNamespace',
+ parent_id: group1.id)
+ end
+
+ let!(:project2) do
+ projects_table.create!(name: 'splendiferous project', path: 'project-path-2', project_namespace_id: project_namespace2.id,
+ namespace_id: group1.id)
+ end
+
+ # create project member records, a mix of both valid and invalid
+ # group members will have already been filtered out.
+ let!(:project_member1) { create_invalid_project_member(id: 1, user_id: user1.id) }
+ let!(:project_member2) { create_valid_project_member(id: 4, user_id: user2.id, project: project1) }
+ let!(:project_member3) { create_valid_project_member(id: 5, user_id: user3.id, project: project2) }
+ let!(:project_member4) { create_invalid_project_member(id: 6, user_id: user4.id) }
+ let!(:project_member5) { create_valid_project_member(id: 7, user_id: user5.id, project: project2) }
+ let!(:project_member6) { create_invalid_project_member(id: 8, user_id: user6.id) }
+
+ it 'removes invalid memberships but keeps valid ones', :aggregate_failures do
+ expect(members_table.where(type: 'ProjectMember').count).to eq 6
+
+ queries = ActiveRecord::QueryRecorder.new do
+ perform_migration
+ end
+
+ expect(queries.count).to eq(4)
+ expect(members_table.where(type: 'ProjectMember')).to match_array([project_member2, project_member3, project_member5])
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+
+ it 'logs IDs of deleted records' do
+ expect(Gitlab::AppLogger).to receive(:info).with({ message: 'Removing invalid project member records',
+ deleted_count: 3, ids: [project_member1, project_member4, project_member6].map(&:id) })
+
+ perform_migration
+ end
+
+ def create_invalid_project_member(id:, user_id:)
+ members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember", source_type: "Project", notification_level: 3, member_namespace_id: nil)
+ end
+
+ def create_valid_project_member(id:, user_id:, project:)
+ members_table.create!(id: id, user_id: user_id, source_id: project.id, access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember", source_type: "Project", member_namespace_id: project.project_namespace_id, notification_level: 3)
+ end
+ # rubocop: enable Layout/LineLength
+ # rubocop: enable RSpec/ScatteredLet
+end
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
new file mode 100644
index 00000000000..7edba8cf524
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenceForRecentPublicProjects, :migration do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:namespace_1) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-1') }
+ let(:project_namespace_2) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let(:project_namespace_3) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-3', type: 'Project') }
+ let(:project_namespace_4) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') }
+ let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-5', type: 'Project') }
+ let(:project_namespace_6) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-6', type: 'Project') }
+ let(:project_namespace_7) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-7', type: 'Project') }
+ let(:project_namespace_8) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-8', type: 'Project') }
+
+ let(:project_1) do
+ projects_table
+ .create!(
+ name: 'proj-1', path: 'path-1', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_2.id, visibility_level: 0
+ )
+ end
+
+ let(:project_2) do
+ projects_table
+ .create!(
+ name: 'proj-2', path: 'path-2', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_3.id, visibility_level: 10, created_at: '2022-02-22'
+ )
+ end
+
+ let(:project_3) do
+ projects_table
+ .create!(
+ name: 'proj-3', path: 'path-3', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_4.id, visibility_level: 20, created_at: '2022-02-17 09:00:01'
+ )
+ end
+
+ let(:project_4) do
+ projects_table
+ .create!(
+ name: 'proj-4', path: 'path-4', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_5.id, visibility_level: 20, created_at: '2022-02-01'
+ )
+ end
+
+ let(:project_5) do
+ projects_table
+ .create!(
+ name: 'proj-5', path: 'path-5', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_6.id, visibility_level: 20, created_at: '2022-01-04'
+ )
+ end
+
+ let(:project_6) do
+ projects_table
+ .create!(
+ name: 'proj-6', path: 'path-6', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_7.id, visibility_level: 20, created_at: '2022-02-17 08:59:59'
+ )
+ end
+
+ let(:project_7) do
+ projects_table
+ .create!(
+ name: 'proj-7', path: 'path-7', namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_8.id, visibility_level: 20, created_at: '2022-02-17 09:00:00'
+ )
+ end
+
+ let(:projects_table) { table(:projects) }
+ let(:project_settings_table) { table(:project_settings) }
+
+ subject(:perform_migration) do
+ described_class.new(start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection)
+ .perform
+ end
+
+ before do
+ project_settings_table.create!(project_id: project_1.id, legacy_open_source_license_available: true)
+ project_settings_table.create!(project_id: project_2.id, legacy_open_source_license_available: true)
+ project_settings_table.create!(project_id: project_3.id, legacy_open_source_license_available: true)
+ project_settings_table.create!(project_id: project_4.id, legacy_open_source_license_available: true)
+ project_settings_table.create!(project_id: project_5.id, legacy_open_source_license_available: false)
+ project_settings_table.create!(project_id: project_6.id, legacy_open_source_license_available: true)
+ project_settings_table.create!(project_id: project_7.id, legacy_open_source_license_available: true)
+ end
+
+ it 'sets `legacy_open_source_license_available` attribute to false for public projects created after threshold time',
+ :aggregate_failures do
+ record = ActiveRecord::QueryRecorder.new do
+ expect { perform_migration }
+ .to not_change { migrated_attribute(project_1.id) }.from(true)
+ .and not_change { migrated_attribute(project_2.id) }.from(true)
+ .and change { migrated_attribute(project_3.id) }.from(true).to(false)
+ .and not_change { migrated_attribute(project_4.id) }.from(true)
+ .and not_change { migrated_attribute(project_5.id) }.from(false)
+ .and not_change { migrated_attribute(project_6.id) }.from(true)
+ .and change { migrated_attribute(project_7.id) }.from(true).to(false)
+ end
+ expect(record.count).to eq(19)
+ end
+
+ def migrated_attribute(project_id)
+ project_settings_table.find(project_id).legacy_open_source_license_available
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
new file mode 100644
index 00000000000..205350f9df4
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanOneMb,
+ :migration,
+ schema: 20220906074449 do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:project_settings_table) { table(:project_settings) }
+ let(:project_statistics_table) { table(:project_statistics) }
+
+ subject(:perform_migration) do
+ described_class.new(start_id: project_settings_table.minimum(:project_id),
+ end_id: project_settings_table.maximum(:project_id),
+ batch_table: :project_settings,
+ batch_column: :project_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection)
+ .perform
+ end
+
+ it 'sets `legacy_open_source_license_available` to false only for projects less than 1 MB',
+ :aggregate_failures do
+ project_setting_1_mb = create_legacy_license_project_setting(repo_size: 1)
+ project_setting_2_mb = create_legacy_license_project_setting(repo_size: 2)
+ project_setting_quarter_mb = create_legacy_license_project_setting(repo_size: 0.25)
+ project_setting_half_mb = create_legacy_license_project_setting(repo_size: 0.5)
+
+ queries = ActiveRecord::QueryRecorder.new { perform_migration }
+
+ expect(queries.count).to eq(7)
+ expect(migrated_attribute(project_setting_1_mb)).to be_truthy
+ expect(migrated_attribute(project_setting_2_mb)).to be_truthy
+ expect(migrated_attribute(project_setting_quarter_mb)).to be_falsey
+ expect(migrated_attribute(project_setting_half_mb)).to be_falsey
+ end
+
+ private
+
+ # @param repo_size: Repo size in MB
+ def create_legacy_license_project_setting(repo_size:)
+ path = "path-for-repo-size-#{repo_size}"
+ namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
+ project_namespace =
+ namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
+ project = projects_table
+ .create!(name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id)
+
+ size_in_bytes = 1.megabyte * repo_size
+ project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes)
+ project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true)
+ end
+
+ def migrated_attribute(project_setting)
+ project_settings_table.find(project_setting.project_id).legacy_open_source_license_available
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb
index 38e8b159e63..c788b701d79 100644
--- a/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb
+++ b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb
@@ -40,10 +40,10 @@ RSpec.describe Gitlab::BackgroundMigration::EncryptIntegrationProperties, schema
expect(integrations.count).to eq(4)
expect(props).to match(
- no_properties.id => both(be_nil),
+ no_properties.id => both(be_nil),
with_plaintext_1.id => both(eq some_props(1)),
with_plaintext_2.id => both(eq some_props(2)),
- with_encrypted.id => match([be_nil, eq(some_props(3))])
+ with_encrypted.id => match([be_nil, eq(some_props(3))])
)
end
diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
new file mode 100644
index 00000000000..41266cb24da
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt do
+ it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchedMigrationJob }
+
+ describe '#perform' do
+ let(:job_artifact) { table(:ci_job_artifacts, database: :ci) }
+
+ let(:test_worker) do
+ described_class.new(
+ start_id: 1,
+ end_id: 100,
+ batch_table: :ci_job_artifacts,
+ batch_column: :id,
+ sub_batch_size: 10,
+ pause_ms: 0,
+ connection: Ci::ApplicationRecord.connection
+ )
+ end
+
+ let_it_be(:namespace) { table(:namespaces).create!(id: 1, name: 'user', path: 'user') }
+ let_it_be(:project) do
+ table(:projects).create!(
+ id: 1,
+ name: 'gitlab1',
+ path: 'gitlab1',
+ project_namespace_id: 1,
+ namespace_id: namespace.id
+ )
+ end
+
+ subject { test_worker.perform }
+
+ context 'with artifacts that has backfilled expire_at' do
+ let!(:created_on_00_30_45_minutes_on_21_22_23) do
+ create_job_artifact(id: 1, file_type: 1, expire_at: Time.zone.parse('2022-01-21 00:00:00.000'))
+ create_job_artifact(id: 2, file_type: 1, expire_at: Time.zone.parse('2022-01-21 01:30:00.000'))
+ create_job_artifact(id: 3, file_type: 1, expire_at: Time.zone.parse('2022-01-22 12:00:00.000'))
+ create_job_artifact(id: 4, file_type: 1, expire_at: Time.zone.parse('2022-01-22 12:30:00.000'))
+ create_job_artifact(id: 5, file_type: 1, expire_at: Time.zone.parse('2022-01-23 23:00:00.000'))
+ create_job_artifact(id: 6, file_type: 1, expire_at: Time.zone.parse('2022-01-23 23:30:00.000'))
+ create_job_artifact(id: 7, file_type: 1, expire_at: Time.zone.parse('2022-01-23 06:45:00.000'))
+ end
+
+ let!(:created_close_to_00_or_30_minutes) do
+ create_job_artifact(id: 8, file_type: 1, expire_at: Time.zone.parse('2022-01-21 00:00:00.001'))
+ create_job_artifact(id: 9, file_type: 1, expire_at: Time.zone.parse('2022-01-21 00:30:00.999'))
+ end
+
+ let!(:created_on_00_or_30_minutes_on_other_dates) do
+ create_job_artifact(id: 10, file_type: 1, expire_at: Time.zone.parse('2022-01-01 00:00:00.000'))
+ create_job_artifact(id: 11, file_type: 1, expire_at: Time.zone.parse('2022-01-19 12:00:00.000'))
+ create_job_artifact(id: 12, file_type: 1, expire_at: Time.zone.parse('2022-01-24 23:30:00.000'))
+ end
+
+ let!(:created_at_other_times) do
+ create_job_artifact(id: 13, file_type: 1, expire_at: Time.zone.parse('2022-01-19 00:00:00.000'))
+ create_job_artifact(id: 14, file_type: 1, expire_at: Time.zone.parse('2022-01-19 00:30:00.000'))
+ create_job_artifact(id: 15, file_type: 1, expire_at: Time.zone.parse('2022-01-24 00:00:00.000'))
+ create_job_artifact(id: 16, file_type: 1, expire_at: Time.zone.parse('2022-01-24 00:30:00.000'))
+ end
+
+ it 'removes expire_at on job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
+ expect { subject }.to change { job_artifact.where(expire_at: nil).count }.from(0).to(7)
+ end
+
+ it 'keeps expire_at on other job artifacts' do
+ expect { subject }.to change { job_artifact.where.not(expire_at: nil).count }.from(16).to(9)
+ end
+ end
+
+ context 'with trace artifacts that has backfilled expire_at' do
+ let!(:trace_artifacts) do
+ create_job_artifact(id: 1, file_type: 3, expire_at: Time.zone.parse('2022-01-01 00:00:00.000'))
+ create_job_artifact(id: 2, file_type: 3, expire_at: Time.zone.parse('2022-01-21 00:00:00.000'))
+ end
+
+ it 'removes expire_at on trace job artifacts' do
+ expect { subject }.to change { job_artifact.where(expire_at: nil).count }.from(0).to(2)
+ end
+ end
+
+ private
+
+ def create_job_artifact(id:, file_type:, expire_at:)
+ job = table(:ci_builds, database: :ci).create!(id: id)
+ job_artifact.create!(id: id, job_id: job.id, expire_at: expire_at, project_id: project.id, file_type: file_type)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb b/spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb
new file mode 100644
index 00000000000..81927100562
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RemoveSelfManagedWikiNotes, :migration, schema: 20220601110011 do
+ let(:notes) { table(:notes) }
+
+ subject(:perform_migration) do
+ described_class.new(start_id: 1,
+ end_id: 30,
+ batch_table: :notes,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection)
+ .perform
+ end
+
+ it 'removes all wiki notes' do
+ notes.create!(id: 2, note: 'Commit note', noteable_type: 'Commit')
+ notes.create!(id: 10, note: 'Issue note', noteable_type: 'Issue')
+ notes.create!(id: 20, note: 'Wiki note', noteable_type: 'Wiki')
+ notes.create!(id: 30, note: 'MergeRequest note', noteable_type: 'MergeRequest')
+
+ expect(notes.where(noteable_type: 'Wiki').size).to eq(1)
+
+ expect { perform_migration }.to change(notes, :count).by(-1)
+
+ expect(notes.where(noteable_type: 'Wiki').size).to eq(0)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb b/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb
new file mode 100644
index 00000000000..45932defaf9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RenameTaskSystemNoteToChecklistItem do
+ let(:notes) { table(:notes) }
+ let(:projects) { table(:projects) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'batchtest1', type: 'Group', path: 'space1') }
+ let(:project) { table(:projects).create!(name: 'proj1', path: 'proj1', namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(title: 'Test issue') }
+
+ let!(:note1) do
+ notes.create!(
+ note: 'marked the task **Task 1** as complete', noteable_type: 'Issue', noteable_id: issue.id, system: true
+ )
+ end
+
+ let!(:note2) do
+ notes.create!(
+ note: 'NO_MATCH marked the task **Task 2** as complete',
+ noteable_type: 'Issue',
+ noteable_id: issue.id,
+ system: true
+ )
+ end
+
+ let!(:note3) do
+ notes.create!(
+ note: 'marked the task **Task 3** as incomplete',
+ noteable_type: 'Issue',
+ noteable_id: issue.id,
+ system: true
+ )
+ end
+
+ let!(:note4) do
+ notes.create!(
+ note: 'marked the task **Task 4** as incomplete',
+ noteable_type: 'Issue',
+ noteable_id: issue.id,
+ system: true
+ )
+ end
+
+ let!(:metadata1) { table(:system_note_metadata).create!(note_id: note1.id, action: :task) }
+ let!(:metadata2) { table(:system_note_metadata).create!(note_id: note2.id, action: :task) }
+ let!(:metadata3) { table(:system_note_metadata).create!(note_id: note3.id, action: :task) }
+ let!(:metadata4) { table(:system_note_metadata).create!(note_id: note4.id, action: :not_task) }
+
+ let(:migration) do
+ described_class.new(
+ start_id: metadata1.id,
+ end_id: metadata4.id,
+ batch_table: :system_note_metadata,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 2,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ it 'renames task to checklist item in task system notes that match', :aggregate_failures do
+ expect do
+ perform_migration
+
+ note1.reload
+ note2.reload
+ note3.reload
+ note4.reload
+ end.to change(note1, :note).to('marked the checklist item **Task 1** as complete').and(
+ not_change(note2, :note).from('NO_MATCH marked the task **Task 2** as complete')
+ ).and(
+ change(note3, :note).to('marked the checklist item **Task 3** as incomplete')
+ ).and(
+ not_change(note4, :note).from('marked the task **Task 4** as incomplete')
+ )
+ end
+
+ it 'updates in batches' do
+ expect { perform_migration }.to make_queries_matching(/UPDATE notes/, 2)
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb
index d5b98e49a31..2372ce21c4c 100644
--- a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb
+++ b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do
let(:detected_state) { 1 }
let(:dismissed_state) { 2 }
- subject(:perform_migration) do
+ let(:migration_job) do
described_class.new(start_id: vulnerability_with_dismissed_at.id,
end_id: vulnerability_without_dismissed_at.id,
batch_table: :vulnerabilities,
@@ -42,15 +42,24 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do
sub_batch_size: 1,
pause_ms: 0,
connection: ActiveRecord::Base.connection)
- .perform
end
- it 'changes vulnerability state to `dismissed` when dismissed_at is not nil' do
- expect { perform_migration }.to change { vulnerability_with_dismissed_at.reload.state }.to(dismissed_state)
+ describe '#filter_batch' do
+ it 'filters out vulnerabilities where dismissed_at is null' do
+ expect(migration_job.filter_batch(vulnerabilities)).to contain_exactly(vulnerability_with_dismissed_at)
+ end
end
- it 'does not change the state when dismissed_at is nil' do
- expect { perform_migration }.not_to change { vulnerability_without_dismissed_at.reload.state }
+ describe '#perform' do
+ subject(:perform_migration) { migration_job.perform }
+
+ it 'changes vulnerability state to `dismissed` when dismissed_at is not nil' do
+ expect { perform_migration }.to change { vulnerability_with_dismissed_at.reload.state }.to(dismissed_state)
+ end
+
+ it 'does not change the state when dismissed_at is nil' do
+ expect { perform_migration }.not_to change { vulnerability_without_dismissed_at.reload.state }
+ end
end
private
diff --git a/spec/lib/gitlab/bullet/exclusions_spec.rb b/spec/lib/gitlab/bullet/exclusions_spec.rb
index ba42156b0c4..325b0167f58 100644
--- a/spec/lib/gitlab/bullet/exclusions_spec.rb
+++ b/spec/lib/gitlab/bullet/exclusions_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tempfile'
RSpec.describe Gitlab::Bullet::Exclusions do
let(:config_file) do
diff --git a/spec/lib/gitlab/cache/helpers_spec.rb b/spec/lib/gitlab/cache/helpers_spec.rb
index 08e0d7729bd..39d37e979b4 100644
--- a/spec/lib/gitlab/cache/helpers_spec.rb
+++ b/spec/lib/gitlab/cache/helpers_spec.rb
@@ -18,10 +18,7 @@ RSpec.describe Gitlab::Cache::Helpers, :use_clean_rails_redis_caching do
end
describe "#render_cached" do
- subject do
- instance.render_cached(presentable, **kwargs)
- end
-
+ let(:method) { :render_cached }
let(:kwargs) do
{
with: presenter,
@@ -29,6 +26,10 @@ RSpec.describe Gitlab::Cache::Helpers, :use_clean_rails_redis_caching do
}
end
+ subject do
+ instance.public_send(method, presentable, **kwargs)
+ end
+
context 'single object' do
let_it_be(:presentable) { create(:merge_request, source_project: project, source_branch: 'wip') }
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
index 8292764f561..762a121340e 100644
--- a/spec/lib/gitlab/changes_list_spec.rb
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'fast_spec_helper'
RSpec.describe Gitlab::ChangesList do
let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" }
diff --git a/spec/lib/gitlab/chat/responder/base_spec.rb b/spec/lib/gitlab/chat/responder/base_spec.rb
index 667228cbab4..2a253449678 100644
--- a/spec/lib/gitlab/chat/responder/base_spec.rb
+++ b/spec/lib/gitlab/chat/responder/base_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Chat::Responder::Base do
let(:project) { double(:project) }
diff --git a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
index cf93ebe0721..b11002e8e93 100644
--- a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
# The rest of the specs for this class are covered in style_spec.rb
RSpec.describe Gitlab::Ci::Ansi2json::Parser do
diff --git a/spec/lib/gitlab/ci/ansi2json/result_spec.rb b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
index b7b4d6de8b9..14e2a9625fe 100644
--- a/spec/lib/gitlab/ci/ansi2json/result_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Ansi2json::Result do
let(:stream) { StringIO.new('hello') }
diff --git a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb b/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb
deleted file mode 100644
index 2c236ba3726..00000000000
--- a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Ci::Build::Artifacts::Adapters::ZipStream do
- let(:file_name) { 'single_file.zip' }
- let(:fixture_path) { "lib/gitlab/ci/build/artifacts/adapters/zip_stream/#{file_name}" }
- let(:stream) { File.open(expand_fixture_path(fixture_path), 'rb') }
-
- describe '#initialize' do
- it 'initializes when stream is passed' do
- expect { described_class.new(stream) }.not_to raise_error
- end
-
- context 'when stream is not passed' do
- let(:stream) { nil }
-
- it 'raises an error' do
- expect { described_class.new(stream) }.to raise_error(described_class::InvalidStreamError)
- end
- end
- end
-
- describe '#each_blob' do
- let(:adapter) { described_class.new(stream) }
-
- context 'when stream is a zip file' do
- it 'iterates file content when zip file contains one file' do
- expect { |b| adapter.each_blob(&b) }
- .to yield_with_args("file 1 content\n")
- end
-
- context 'when zip file contains multiple files' do
- let(:file_name) { 'multiple_files.zip' }
-
- it 'iterates content of all files' do
- expect { |b| adapter.each_blob(&b) }
- .to yield_successive_args("file 1 content\n", "file 2 content\n")
- end
- end
-
- context 'when zip file includes files in a directory' do
- let(:file_name) { 'with_directory.zip' }
-
- it 'iterates contents from files only' do
- expect { |b| adapter.each_blob(&b) }
- .to yield_successive_args("file 1 content\n", "file 2 content\n")
- end
- end
-
- context 'when zip contains a file which decompresses beyond the size limit' do
- let(:file_name) { '200_mb_decompressed.zip' }
-
- it 'does not read the file' do
- expect { |b| adapter.each_blob(&b) }.not_to yield_control
- end
- end
-
- context 'when the zip contains too many files' do
- let(:file_name) { '100_files.zip' }
-
- it 'stops processing when the limit is reached' do
- expect { |b| adapter.each_blob(&b) }
- .to yield_control.exactly(described_class::MAX_FILES_PROCESSED).times
- end
- end
-
- context 'when stream is a zipbomb' do
- let(:file_name) { 'zipbomb.zip' }
-
- it 'does not read the file' do
- expect { |b| adapter.each_blob(&b) }.not_to yield_control
- end
- end
- end
-
- context 'when stream is not a zip file' do
- let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') }
-
- it 'does not yield any data' do
- expect { |b| adapter.each_blob(&b) }.not_to yield_control
- expect { adapter.each_blob { |b| b } }.not_to raise_error
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb
index 27b7dac2ae4..773eaf4b3fc 100644
--- a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Build::Artifacts::Path do
describe '#valid?' do
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index 436ad59bdf7..e560f1c2b5a 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -108,7 +108,8 @@ RSpec.describe Gitlab::Ci::Build::Policy::Variables do
project: project,
ref: 'master',
stage: 'review',
- environment: 'test/$CI_JOB_STAGE/1')
+ environment: 'test/$CI_JOB_STAGE/1',
+ ci_stage: build(:ci_stage, name: 'review', project: project, pipeline: pipeline))
end
before do
diff --git a/spec/lib/gitlab/ci/build/policy_spec.rb b/spec/lib/gitlab/ci/build/policy_spec.rb
index b85b093fd03..4baf4a1b4c4 100644
--- a/spec/lib/gitlab/ci/build/policy_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Build::Policy do
let(:policy) { spy('policy specification') }
diff --git a/spec/lib/gitlab/ci/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb
index 480418e0851..51820c1ab2c 100644
--- a/spec/lib/gitlab/ci/build/port_spec.rb
+++ b/spec/lib/gitlab/ci/build/port_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Build::Port do
subject { described_class.new(port) }
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
index f192862c1c4..f9ebab149a5 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
@@ -3,44 +3,54 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
- shared_examples 'an exists rule with a context' do
- subject { described_class.new(globs).satisfied_by?(pipeline, context) }
+ describe '#satisfied_by?' do
+ shared_examples 'an exists rule with a context' do
+ it_behaves_like 'a glob matching rule' do
+ let(:project) { create(:project, :custom_repo, files: files) }
+ end
- it_behaves_like 'a glob matching rule' do
- let(:project) { create(:project, :custom_repo, files: files) }
- end
+ context 'after pattern comparision limit is reached' do
+ let(:globs) { ['*definitely_not_a_matching_glob*'] }
+ let(:project) { create(:project, :repository) }
- context 'after pattern comparision limit is reached' do
- let(:globs) { ['*definitely_not_a_matching_glob*'] }
- let(:project) { create(:project, :repository) }
+ before do
+ stub_const('Gitlab::Ci::Build::Rules::Rule::Clause::Exists::MAX_PATTERN_COMPARISONS', 2)
+ expect(File).to receive(:fnmatch?).twice.and_call_original
+ end
- before do
- stub_const('Gitlab::Ci::Build::Rules::Rule::Clause::Exists::MAX_PATTERN_COMPARISONS', 2)
- expect(File).to receive(:fnmatch?).twice.and_call_original
+ it { is_expected.to be_truthy }
end
-
- it { is_expected.to be_truthy }
end
- end
- describe '#satisfied_by?' do
- let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
+ subject(:satisfied_by?) { described_class.new(globs).satisfied_by?(nil, context) }
context 'when context is Build::Context::Build' do
it_behaves_like 'an exists rule with a context' do
- let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: 'abc1234') }
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) }
+ let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: project.repository.commit.sha) }
end
end
context 'when context is Build::Context::Global' do
it_behaves_like 'an exists rule with a context' do
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) }
let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) }
end
end
context 'when context is Config::External::Context' do
+ let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: sha) }
+
it_behaves_like 'an exists rule with a context' do
- let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: project.repository.tree.sha) }
+ let(:sha) { project.repository.commit.sha }
+ end
+
+ context 'when context has no project' do
+ let(:globs) { ['Dockerfile'] }
+ let(:project) {}
+ let(:sha) { 'abc1234' }
+
+ it { is_expected.to eq(false) }
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 36c26c8ee4f..3562706ff33 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -230,12 +230,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
end
end
- context 'when auto_stop_in is invalid format' do
- let(:auto_stop_in) { 'invalid' }
+ context 'when variables are used for auto_stop_in' do
+ let(:auto_stop_in) { '$TTL' }
- it 'becomes invalid' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include 'environment auto stop in should be a duration'
+ it 'becomes valid' do
+ expect(entry).to be_valid
+ expect(entry.auto_stop_in).to eq(auto_stop_in)
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index 6121c28070f..b37498ba10a 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -4,8 +4,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Image do
before do
- stub_feature_flags(ci_docker_image_pull_policy: true)
-
entry.compose!
end
@@ -129,18 +127,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
it 'is valid' do
expect(entry).to be_valid
end
-
- context 'when the feature flag ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- entry.compose!
- end
-
- it 'is not valid' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include('image config contains unknown keys: pull_policy')
- end
- end
end
describe '#value' do
@@ -150,19 +136,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
pull_policy: ['if-not-present']
)
end
-
- context 'when the feature flag ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- entry.compose!
- end
-
- it 'is not valid' do
- expect(entry.value).to eq(
- name: 'image:1.0'
- )
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index ca336c3ecaa..75ac2ca87ab 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -605,8 +605,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
let(:deps) do
double('deps',
'default_entry' => default,
- 'workflow_entry' => workflow,
- 'variables_value' => nil)
+ 'workflow_entry' => workflow)
end
context 'when job config overrides default config' do
diff --git a/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb b/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb
new file mode 100644
index 00000000000..e9edec9a0a4
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::LegacyVariables do
+ let(:config) { {} }
+ let(:metadata) { {} }
+
+ subject(:entry) { described_class.new(config, **metadata) }
+
+ before do
+ entry.compose!
+ end
+
+ shared_examples 'valid config' do
+ describe '#value' do
+ it 'returns hash with key value strings' do
+ expect(entry.value).to eq result
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ shared_examples 'invalid config' do |error_message|
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include(error_message)
+ end
+ end
+ end
+
+ context 'when entry config value has key-value pairs' do
+ let(:config) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
+
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
+
+ it_behaves_like 'valid config'
+
+ describe '#value_with_data' do
+ it 'returns variable with data' do
+ expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value 1' },
+ 'VARIABLE_2' => { value: 'value 2' }
+ )
+ end
+ end
+ end
+
+ context 'with numeric keys and values in the config' do
+ let(:config) { { 10 => 20 } }
+ let(:result) do
+ { '10' => '20' }
+ end
+
+ it_behaves_like 'valid config'
+ end
+
+ context 'when key is an array' do
+ let(:config) { { ['VAR1'] => 'val1' } }
+ let(:result) do
+ { 'VAR1' => 'val1' }
+ end
+
+ it_behaves_like 'invalid config', /should be a hash of key value pairs/
+ end
+
+ context 'when value is a symbol' do
+ let(:config) { { 'VAR1' => :val1 } }
+ let(:result) do
+ { 'VAR1' => 'val1' }
+ end
+
+ it_behaves_like 'valid config'
+ end
+
+ context 'when value is a boolean' do
+ let(:config) { { 'VAR1' => true } }
+ let(:result) do
+ { 'VAR1' => 'val1' }
+ end
+
+ it_behaves_like 'invalid config', /should be a hash of key value pairs/
+ end
+
+ context 'when entry config value has key-value pair and hash' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
+ 'VARIABLE_2' => 'value 2' }
+ end
+
+ it_behaves_like 'invalid config', /should be a hash of key value pairs/
+
+ context 'when metadata has use_value_data: true' do
+ let(:metadata) { { use_value_data: true } }
+
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
+
+ it_behaves_like 'valid config'
+
+ describe '#value_with_data' do
+ it 'returns variable with data' do
+ expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
+ 'VARIABLE_2' => { value: 'value 2' }
+ )
+ end
+ end
+ end
+ end
+
+ context 'when entry value is an array' do
+ let(:config) { [:VAR, 'test'] }
+
+ it_behaves_like 'invalid config', /should be a hash of key value pairs/
+ end
+
+ context 'when metadata has use_value_data: true' do
+ let(:metadata) { { use_value_data: true } }
+
+ context 'when entry value has hash with other key-pairs' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' },
+ 'VARIABLE_2' => 'value 2' }
+ end
+
+ it_behaves_like 'invalid config', /should be a hash of key value pairs, value can be a hash/
+ end
+
+ context 'when entry config value has hash with nil description' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1', description: nil } }
+ end
+
+ it_behaves_like 'invalid config', /should be a hash of key value pairs, value can be a hash/
+ end
+
+ context 'when entry config value has hash without description' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1' } }
+ end
+
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1' }
+ end
+
+ it_behaves_like 'valid config'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb
index e2840c07f6b..77f846f95f0 100644
--- a/spec/lib/gitlab/ci/config/entry/port_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Port do
let(:config) do
{ number: 80,
protocol: 'http',
- name: 'foobar' }
+ name: 'foobar' }
end
describe '#valid?' do
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index 714b0a3b6aa..5f42a8c49a7 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -197,6 +197,34 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
end
end
+
+ context 'when a variable has an invalid data attribute' do
+ let(:config) do
+ {
+ script: 'echo',
+ variables: { 'VAR1' => 'val 1', 'VAR2' => { value: 'val 2', description: 'hello var 2' } }
+ }
+ end
+
+ it 'reports error about variable' do
+ expect(entry.errors)
+ .to include 'variables:var2 config must be a string'
+ end
+
+ context 'when the FF ci_variables_refactoring_to_variable is disabled' do
+ let(:entry_without_ff) { node_class.new(config, name: :rspec) }
+
+ before do
+ stub_feature_flags(ci_variables_refactoring_to_variable: false)
+ entry_without_ff.compose!
+ end
+
+ it 'reports error about variable' do
+ expect(entry_without_ff.errors)
+ .to include /config should be a hash of key value pairs/
+ end
+ end
+ end
end
end
@@ -212,13 +240,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
let(:unspecified) { double('unspecified', 'specified?' => false) }
let(:default) { double('default', '[]' => unspecified) }
let(:workflow) { double('workflow', 'has_rules?' => false) }
- let(:variables) {}
let(:deps) do
double('deps',
default_entry: default,
- workflow_entry: workflow,
- variables_value: variables)
+ workflow_entry: workflow)
end
context 'with workflow rules' do
diff --git a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb
index ff44a235ea5..394d91466bf 100644
--- a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
require_dependency 'active_model'
RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 55ad119ea21..3d19987a0be 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -117,49 +117,49 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value.keys).to eq([:rspec, :spinach, :release])
expect(root.jobs_value[:rspec]).to eq(
{ name: :rspec,
- script: %w[rspec ls],
- before_script: %w(ls pwd),
- image: { name: 'image:1.0' },
- services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
- stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- job_variables: {},
- root_variables_inheritance: true,
- ignore: false,
- after_script: ['make clean'],
- only: { refs: %w[branches tags] },
- scheduling_type: :stage }
+ script: %w[rspec ls],
+ before_script: %w(ls pwd),
+ image: { name: 'image:1.0' },
+ services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
+ stage: 'test',
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ job_variables: {},
+ root_variables_inheritance: true,
+ ignore: false,
+ after_script: ['make clean'],
+ only: { refs: %w[branches tags] },
+ scheduling_type: :stage }
)
expect(root.jobs_value[:spinach]).to eq(
{ name: :spinach,
- before_script: [],
- script: %w[spinach],
- image: { name: 'image:1.0' },
- services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
- stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- job_variables: {},
- root_variables_inheritance: true,
- ignore: false,
- after_script: ['make clean'],
- only: { refs: %w[branches tags] },
- scheduling_type: :stage }
+ before_script: [],
+ script: %w[spinach],
+ image: { name: 'image:1.0' },
+ services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
+ stage: 'test',
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ job_variables: {},
+ root_variables_inheritance: true,
+ ignore: false,
+ after_script: ['make clean'],
+ only: { refs: %w[branches tags] },
+ scheduling_type: :stage }
)
expect(root.jobs_value[:release]).to eq(
{ name: :release,
- stage: 'release',
- before_script: [],
- script: ["make changelog | tee release_changelog.txt"],
- release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" },
- image: { name: "image:1.0" },
- services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
- cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }],
- only: { refs: %w(branches tags) },
- job_variables: { 'VAR' => 'job' },
- root_variables_inheritance: true,
- after_script: [],
- ignore: false,
- scheduling_type: :stage }
+ stage: 'release',
+ before_script: [],
+ script: ["make changelog | tee release_changelog.txt"],
+ release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" },
+ image: { name: "image:1.0" },
+ services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
+ cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }],
+ only: { refs: %w(branches tags) },
+ job_variables: { 'VAR' => { value: 'job' } },
+ root_variables_inheritance: true,
+ after_script: [],
+ ignore: false,
+ scheduling_type: :stage }
)
end
end
@@ -196,31 +196,31 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
it 'returns jobs configuration' do
expect(root.jobs_value).to eq(
rspec: { name: :rspec,
- script: %w[rspec ls],
- before_script: %w(ls pwd),
- image: { name: 'image:1.0' },
- services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
- stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- job_variables: {},
- root_variables_inheritance: true,
- ignore: false,
- after_script: ['make clean'],
- only: { refs: %w[branches tags] },
- scheduling_type: :stage },
+ script: %w[rspec ls],
+ before_script: %w(ls pwd),
+ image: { name: 'image:1.0' },
+ services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
+ stage: 'test',
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ job_variables: {},
+ root_variables_inheritance: true,
+ ignore: false,
+ after_script: ['make clean'],
+ only: { refs: %w[branches tags] },
+ scheduling_type: :stage },
spinach: { name: :spinach,
- before_script: [],
- script: %w[spinach],
- image: { name: 'image:1.0' },
- services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
- stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- job_variables: { 'VAR' => 'job' },
- root_variables_inheritance: true,
- ignore: false,
- after_script: ['make clean'],
- only: { refs: %w[branches tags] },
- scheduling_type: :stage }
+ before_script: [],
+ script: %w[spinach],
+ image: { name: 'image:1.0' },
+ services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
+ stage: 'test',
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ job_variables: { 'VAR' => { value: 'job' } },
+ root_variables_inheritance: true,
+ ignore: false,
+ after_script: ['make clean'],
+ only: { refs: %w[branches tags] },
+ scheduling_type: :stage }
)
end
end
@@ -350,6 +350,33 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
end
end
end
+
+ context 'when a variable has an invalid data key' do
+ let(:hash) do
+ { variables: { VAR1: { invalid: 'hello' } }, rspec: { script: 'hello' } }
+ end
+
+ describe '#errors' do
+ it 'reports errors about the invalid variable' do
+ expect(root.errors)
+ .to include /var1 config uses invalid data keys: invalid/
+ end
+
+ context 'when the FF ci_variables_refactoring_to_variable is disabled' do
+ let(:root_without_ff) { described_class.new(hash, user: user, project: project) }
+
+ before do
+ stub_feature_flags(ci_variables_refactoring_to_variable: false)
+ root_without_ff.compose!
+ end
+
+ it 'reports errors about the invalid variable' do
+ expect(root_without_ff.errors)
+ .to include /variables config should be a hash of key value pairs, value can be a hash/
+ end
+ end
+ end
+ end
end
context 'when value is not a hash' do
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index c85fe366da6..303d825c591 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'gitlab_chronic_duration'
-require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do
let(:factory) do
@@ -363,7 +362,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do
it { is_expected.not_to be_valid }
it 'returns an error about invalid variables:' do
- expect(subject.errors).to include(/variables config should be a hash of key value pairs/)
+ expect(subject.errors).to include(/variables config should be a hash/)
+ end
+
+ context 'when the FF ci_variables_refactoring_to_variable is disabled' do
+ let(:entry_without_ff) { factory.create! }
+
+ before do
+ stub_feature_flags(ci_variables_refactoring_to_variable: false)
+ entry_without_ff.compose!
+ end
+
+ it 'returns an error about invalid variables:' do
+ expect(subject.errors).to include(/variables config should be a hash/)
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index 821ab442d61..e36484bb0ae 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Service do
before do
- stub_feature_flags(ci_docker_image_pull_policy: true)
entry.compose!
end
@@ -149,18 +148,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
it 'is valid' do
expect(entry).to be_valid
end
-
- context 'when the feature flag ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- entry.compose!
- end
-
- it 'is not valid' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include('service config contains unknown keys: pull_policy')
- end
- end
end
describe '#value' do
@@ -170,18 +157,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
pull_policy: ['if-not-present']
)
end
-
- context 'when the feature flag ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it 'is not valid' do
- expect(entry.value).to eq(
- name: 'postgresql:9.5'
- )
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb
new file mode 100644
index 00000000000..744a89d4509
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::Variable do
+ let(:config) { {} }
+ let(:metadata) { {} }
+
+ subject(:entry) do
+ described_class.new(config, **metadata).tap do |entry|
+ entry.key = 'VAR1' # composable_hash requires key to be set
+ end
+ end
+
+ before do
+ entry.compose!
+ end
+
+ describe 'SimpleVariable' do
+ context 'when config is a string' do
+ let(:config) { 'value' }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('value') }
+ end
+ end
+
+ context 'when config is an integer' do
+ let(:config) { 1 }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('1') }
+ end
+ end
+
+ context 'when config is an array' do
+ let(:config) { [] }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'variable definition must be either a string or a hash' }
+ end
+ end
+ end
+
+ describe 'ComplexVariable' do
+ context 'when config is a hash with description' do
+ let(:config) { { value: 'value', description: 'description' } }
+
+ context 'when metadata allowed_value_data is not provided' do
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config must be a string' }
+ end
+ end
+
+ context 'when metadata allowed_value_data is (value, description)' do
+ let(:metadata) { { allowed_value_data: %i[value description] } }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('value') }
+ end
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: 'value', description: 'description') }
+ end
+
+ context 'when config value is a symbol' do
+ let(:config) { { value: :value, description: 'description' } }
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('value') }
+ end
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: 'value', description: 'description') }
+ end
+ end
+
+ context 'when config value is an integer' do
+ let(:config) { { value: 123, description: 'description' } }
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('123') }
+ end
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: '123', description: 'description') }
+ end
+ end
+
+ context 'when config value is an array' do
+ let(:config) { { value: ['value'], description: 'description' } }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config value must be an alphanumeric string' }
+ end
+ end
+
+ context 'when config description is a symbol' do
+ let(:config) { { value: 'value', description: :description } }
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('value') }
+ end
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: 'value', description: :description) }
+ end
+ end
+ end
+
+ context 'when metadata allowed_value_data is (value, xyz)' do
+ let(:metadata) { { allowed_value_data: %i[value xyz] } }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config uses invalid data keys: description' }
+ end
+ end
+ end
+
+ context 'when config is a hash without description' do
+ let(:config) { { value: 'value' } }
+
+ context 'when metadata allowed_value_data is not provided' do
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config must be a string' }
+ end
+ end
+
+ context 'when metadata allowed_value_data is (value, description)' do
+ let(:metadata) { { allowed_value_data: %i[value description] } }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('value') }
+ end
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: 'value') }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 78d37e228df..ad7290d0589 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -3,41 +3,46 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Variables do
+ let(:config) { {} }
let(:metadata) { {} }
- subject { described_class.new(config, **metadata) }
+ subject(:entry) { described_class.new(config, **metadata) }
+
+ before do
+ entry.compose!
+ end
shared_examples 'valid config' do
describe '#value' do
it 'returns hash with key value strings' do
- expect(subject.value).to eq result
+ expect(entry.value).to eq result
end
end
describe '#errors' do
it 'does not append errors' do
- expect(subject.errors).to be_empty
+ expect(entry.errors).to be_empty
end
end
describe '#valid?' do
it 'is valid' do
- expect(subject).to be_valid
+ expect(entry).to be_valid
end
end
end
- shared_examples 'invalid config' do
+ shared_examples 'invalid config' do |error_message|
describe '#valid?' do
it 'is not valid' do
- expect(subject).not_to be_valid
+ expect(entry).not_to be_valid
end
end
describe '#errors' do
it 'saves errors' do
- expect(subject.errors)
- .to include /should be a hash of key value pairs/
+ expect(entry.errors)
+ .to include(error_message)
end
end
end
@@ -52,6 +57,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
end
it_behaves_like 'valid config'
+
+ describe '#value_with_data' do
+ it 'returns variable with data' do
+ expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value 1' },
+ 'VARIABLE_2' => { value: 'value 2' }
+ )
+ end
+ end
end
context 'with numeric keys and values in the config' do
@@ -63,33 +77,63 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
it_behaves_like 'valid config'
end
+ context 'when key is an array' do
+ let(:config) { { ['VAR1'] => 'val1' } }
+
+ it_behaves_like 'invalid config', /must be an alphanumeric string/
+ end
+
+ context 'when value is a symbol' do
+ let(:config) { { 'VAR1' => :val1 } }
+ let(:result) do
+ { 'VAR1' => 'val1' }
+ end
+
+ it_behaves_like 'valid config'
+ end
+
+ context 'when value is a boolean' do
+ let(:config) { { 'VAR1' => true } }
+
+ it_behaves_like 'invalid config', /must be either a string or a hash/
+ end
+
context 'when entry config value has key-value pair and hash' do
let(:config) do
{ 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
'VARIABLE_2' => 'value 2' }
end
- let(:result) do
- { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
- end
+ it_behaves_like 'invalid config', /variable_1 config must be a string/
- it_behaves_like 'invalid config'
+ context 'when metadata has allowed_value_data' do
+ let(:metadata) { { allowed_value_data: %i[value description] } }
- context 'when metadata has use_value_data' do
- let(:metadata) { { use_value_data: true } }
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
it_behaves_like 'valid config'
+
+ describe '#value_with_data' do
+ it 'returns variable with data' do
+ expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
+ 'VARIABLE_2' => { value: 'value 2' }
+ )
+ end
+ end
end
end
context 'when entry value is an array' do
let(:config) { [:VAR, 'test'] }
- it_behaves_like 'invalid config'
+ it_behaves_like 'invalid config', /variables config should be a hash/
end
- context 'when metadata has use_value_data' do
- let(:metadata) { { use_value_data: true } }
+ context 'when metadata has allowed_value_data' do
+ let(:metadata) { { allowed_value_data: %i[value description] } }
context 'when entry value has hash with other key-pairs' do
let(:config) do
@@ -97,7 +141,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
'VARIABLE_2' => 'value 2' }
end
- it_behaves_like 'invalid config'
+ it_behaves_like 'invalid config', /variable_1 config uses invalid data keys: hello/
end
context 'when entry config value has hash with nil description' do
@@ -105,7 +149,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
{ 'VARIABLE_1' => { value: 'value 1', description: nil } }
end
- it_behaves_like 'invalid config'
+ it_behaves_like 'invalid config', /variable_1 config description must be an alphanumeric string/
end
context 'when entry config value has hash without description' do
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index 45dfea636f3..c22afb32756 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -219,4 +219,43 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do
)
}
end
+
+ describe '#to_hash' do
+ subject(:to_hash) { remote_file.to_hash }
+
+ before do
+ stub_full_request(location).to_return(body: remote_file_content)
+ end
+
+ context 'with a valid remote file' do
+ it 'returns the content as a hash' do
+ expect(to_hash).to eql(
+ before_script: ["apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs",
+ "ruby -v",
+ "which ruby",
+ "bundle install --jobs $(nproc) \"${FLAGS[@]}\""]
+ )
+ end
+ end
+
+ context 'when it has `include` with rules:exists' do
+ let(:remote_file_content) do
+ <<~HEREDOC
+ include:
+ - local: another-file.yml
+ rules:
+ - exists: [Dockerfile]
+ HEREDOC
+ end
+
+ it 'returns the content as a hash' do
+ expect(to_hash).to eql(
+ include: [
+ { local: 'another-file.yml',
+ rules: [{ exists: ['Dockerfile'] }] }
+ ]
+ )
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index e74fdc2071b..9eaba12f388 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -210,7 +210,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
{ 'local' => local_file },
{ 'local' => local_file }
],
- image: 'image:1.0' }
+ image: 'image:1.0' }
end
it 'does not raise an exception' do
@@ -427,7 +427,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
{ 'local' => 'hello/secret-file1.yml' },
{ 'local' => 'hello/secret-file2.yml' }
],
- image: 'ruby:2.7' }
+ image: 'ruby:2.7' }
end
it 'has expanset with two' do
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 841a46e197d..b1dff6f9723 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -94,6 +94,36 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
end
end
+ context 'when the remote file has `include` with rules:exists' do
+ let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
+ let(:values) { { include: remote_file, image: 'image:1.0' } }
+ let(:external_file_content) do
+ <<-HEREDOC
+ include:
+ - local: another-file.yml
+ rules:
+ - exists: [Dockerfile]
+
+ rspec:
+ script:
+ - bundle exec rspec
+ HEREDOC
+ end
+
+ before do
+ stub_full_request(remote_file).to_return(body: external_file_content)
+ end
+
+ it 'evaluates the rule as false' do
+ output = processor.perform
+ expect(output.keys).to match_array([:image, :rspec])
+ end
+
+ it "removes the 'include' keyword" do
+ expect(processor.perform[:include]).to be_nil
+ end
+ end
+
context 'with a valid local external file is defined' do
let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'image:1.0' } }
let(:local_file_content) do
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index 7e0b2b5aa8e..3d46d266c13 100644
--- a/spec/lib/gitlab/ci/lint_spec.rb
+++ b/spec/lib/gitlab/ci/lint_spec.rb
@@ -341,9 +341,9 @@ RSpec.describe Gitlab::Ci::Lint do
let(:counters) do
{
'count' => a_kind_of(Numeric),
- 'avg' => a_kind_of(Numeric),
- 'max' => a_kind_of(Numeric),
- 'min' => a_kind_of(Numeric)
+ 'avg' => a_kind_of(Numeric),
+ 'max' => a_kind_of(Numeric),
+ 'min' => a_kind_of(Numeric)
}
end
diff --git a/spec/lib/gitlab/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb
index 7d950c86700..ffe36e69a8f 100644
--- a/spec/lib/gitlab/ci/mask_secret_spec.rb
+++ b/spec/lib/gitlab/ci/mask_secret_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::MaskSecret do
subject { described_class }
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
index c99cfa94aa6..38b229e0dd8 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do
subject(:parse_source) { described_class.parse_source(properties) }
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
index 431fe9f3591..f3636106b98 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
@@ -102,11 +102,11 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do
it 'adds each component, ignoring unused attributes' do
expect(report).to receive(:add_component)
- .with({ "name" => "activesupport", "version" => "5.1.4", "type" => "library" })
+ .with(an_object_having_attributes(name: "activesupport", version: "5.1.4", component_type: "library"))
expect(report).to receive(:add_component)
- .with({ "name" => "byebug", "version" => "10.0.0", "type" => "library" })
+ .with(an_object_having_attributes(name: "byebug", version: "10.0.0", component_type: "library"))
expect(report).to receive(:add_component)
- .with({ "name" => "minimal-component", "type" => "library" })
+ .with(an_object_having_attributes(name: "minimal-component", version: nil, component_type: "library"))
parse!
end
diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb
index 30114b17cac..7222ebc3cb8 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do
subject { described_class.source(property_data) }
@@ -17,11 +17,11 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do
end
it 'returns expected source data' do
- is_expected.to eq({
- 'type' => :dependency_scanning,
- 'data' => property_data,
- 'fingerprint' => '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188'
- })
+ is_expected.to have_attributes(
+ source_type: :dependency_scanning,
+ data: property_data,
+ fingerprint: '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188'
+ )
end
end
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index 6495d1f654b..297ef1f5bb9 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
}
end
- where(vulnerability_finding_signatures_enabled: [true, false])
+ where(signatures_enabled: [true, false])
with_them do
let_it_be(:pipeline) { create(:ci_pipeline) }
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
let(:validator_class) { Gitlab::Ci::Parsers::Security::Validators::SchemaValidator }
let(:data) { {}.merge(scanner_data) }
let(:json_data) { data.to_json }
- let(:parser) { described_class.new(json_data, report, vulnerability_finding_signatures_enabled, validate: validate) }
+ let(:parser) { described_class.new(json_data, report, signatures_enabled: signatures_enabled, validate: validate) }
subject(:parse_report) { parser.parse! }
@@ -191,7 +191,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
context 'report parsing' do
before do
- artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) }
+ artifact.each_blob { |blob| described_class.parse!(blob, report, signatures_enabled: signatures_enabled) }
end
describe 'parsing finding.name' do
@@ -262,7 +262,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
describe 'top-level scanner' do
it 'is the primary scanner' do
expect(report.primary_scanner.external_id).to eq('gemnasium')
- expect(report.primary_scanner.name).to eq('Gemnasium')
+ expect(report.primary_scanner.name).to eq('Gemnasium top-level')
expect(report.primary_scanner.vendor).to eq('GitLab')
expect(report.primary_scanner.version).to eq('2.18.0')
end
@@ -278,9 +278,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
describe 'parsing scanners' do
subject(:scanner) { report.findings.first.scanner }
- context 'when vendor is not missing in scanner' do
- it 'returns scanner with parsed vendor value' do
- expect(scanner.vendor).to eq('GitLab')
+ context 'when the report contains top-level scanner' do
+ it 'sets the scanner of finding as top-level scanner' do
+ expect(scanner.name).to eq('Gemnasium top-level')
+ end
+ end
+
+ context 'when the report does not contain top-level scanner' do
+ let(:artifact) { build(:ci_job_artifact, :common_security_report_without_top_level_scanner) }
+
+ it 'sets the scanner of finding as `vulnerabilities[].scanner`' do
+ expect(scanner.name).to eq('Gemnasium')
end
end
end
@@ -465,7 +473,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
finding = report.findings.first
highest_signature = finding.signatures.max_by(&:priority)
- identifiers = if vulnerability_finding_signatures_enabled
+ identifiers = if signatures_enabled
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}"
else
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}"
diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
index 7828aa99f6a..e730afc72b5 100644
--- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
@@ -19,8 +19,72 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
+ let(:report_data) do
+ {
+ 'scan' => {
+ 'analyzer' => {
+ 'id' => 'my-dast-analyzer',
+ 'name' => 'My DAST analyzer',
+ 'version' => '0.1.0',
+ 'vendor' => { 'name' => 'A DAST analyzer' }
+ },
+ 'end_time' => '2020-01-28T03:26:02',
+ 'scanned_resources' => [],
+ 'scanner' => {
+ 'id' => 'my-dast-scanner',
+ 'name' => 'My DAST scanner',
+ 'version' => '0.2.0',
+ 'vendor' => { 'name' => 'A DAST scanner' }
+ },
+ 'start_time' => '2020-01-28T03:26:01',
+ 'status' => 'success',
+ 'type' => 'dast'
+ },
+ 'version' => report_version,
+ 'vulnerabilities' => []
+ }
+ end
+
let(:validator) { described_class.new(report_type, report_data, report_version, project: project, scanner: scanner) }
+ shared_examples 'report is valid' do
+ context 'and the report is valid' do
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ shared_examples 'logs related information' do
+ it 'logs related information' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ message: "security report schema validation problem",
+ security_report_type: report_type,
+ security_report_version: report_version,
+ project_id: project.id,
+ security_report_failure: security_report_failure,
+ security_report_scanner_id: 'gemnasium',
+ security_report_scanner_version: '2.1.0'
+ )
+
+ subject
+ end
+ end
+
+ shared_examples 'report is invalid' do
+ context 'and the report is invalid' do
+ let(:report_data) do
+ {
+ 'version' => report_version
+ }
+ end
+
+ let(:security_report_failure) { 'schema_validation_fails' }
+
+ it { is_expected.to be_falsey }
+
+ it_behaves_like 'logs related information'
+ end
+ end
+
describe 'SUPPORTED_VERSIONS' do
schema_path = Rails.root.join("lib", "gitlab", "ci", "parsers", "security", "validators", "schemas")
@@ -75,80 +139,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
(latest_vendored_version[0...2] << "34").join(".")
end
- context 'and the report is valid' do
- let(:report_data) do
- {
- 'version' => report_version,
- 'vulnerabilities' => []
- }
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'and the report is invalid' do
- let(:report_data) do
- {
- 'version' => report_version
- }
- end
-
- it { is_expected.to be_falsey }
-
- it 'logs related information' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "security report schema validation problem",
- security_report_type: report_type,
- security_report_version: report_version,
- project_id: project.id,
- security_report_failure: 'schema_validation_fails',
- security_report_scanner_id: 'gemnasium',
- security_report_scanner_version: '2.1.0'
- )
-
- subject
- end
- end
+ it_behaves_like 'report is valid'
+ it_behaves_like 'report is invalid'
end
context 'when given a supported schema version' do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- context 'and the report is valid' do
- let(:report_data) do
- {
- 'version' => report_version,
- 'vulnerabilities' => []
- }
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'and the report is invalid' do
- let(:report_data) do
- {
- 'version' => report_version
- }
- end
-
- it { is_expected.to be_falsey }
-
- it 'logs related information' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "security report schema validation problem",
- security_report_type: report_type,
- security_report_version: report_version,
- project_id: project.id,
- security_report_failure: 'schema_validation_fails',
- security_report_scanner_id: 'gemnasium',
- security_report_scanner_version: '2.1.0'
- )
-
- subject
- end
- end
+ it_behaves_like 'report is valid'
+ it_behaves_like 'report is invalid'
end
context 'when given a deprecated schema version' do
@@ -173,21 +173,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
+ let(:security_report_failure) { 'using_deprecated_schema_version' }
+
it { is_expected.to be_truthy }
- it 'logs related information' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "security report schema validation problem",
- security_report_type: report_type,
- security_report_version: report_version,
- project_id: project.id,
- security_report_failure: 'using_deprecated_schema_version',
- security_report_scanner_id: 'gemnasium',
- security_report_scanner_version: '2.1.0'
- )
-
- subject
- end
+ it_behaves_like 'logs related information'
end
context 'and the report does not pass schema validation' do
@@ -213,21 +203,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
+ let(:security_report_failure) { 'using_unsupported_schema_version' }
+
it { is_expected.to be_falsey }
- it 'logs related information' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "security report schema validation problem",
- security_report_type: report_type,
- security_report_version: report_version,
- project_id: project.id,
- security_report_failure: 'using_unsupported_schema_version',
- security_report_scanner_id: 'gemnasium',
- security_report_scanner_version: '2.1.0'
- )
-
- subject
- end
+ it_behaves_like 'logs related information'
end
context 'and the report is invalid' do
@@ -282,6 +262,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
+ shared_examples 'report is valid with no error' do
+ context 'and the report is valid' do
+ it { is_expected.to be_empty }
+ end
+ end
+
+ shared_examples 'report with expected errors' do
+ it { is_expected.to match_array(expected_errors) }
+ end
+
describe '#errors' do
subject { validator.errors }
@@ -289,16 +279,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- context 'and the report is valid' do
- let(:report_data) do
- {
- 'version' => report_version,
- 'vulnerabilities' => []
- }
- end
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'report is valid with no error'
context 'and the report is invalid' do
let(:report_data) do
@@ -309,11 +290,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
- 'root is missing required keys: vulnerabilities'
+ 'root is missing required keys: scan, vulnerabilities'
]
end
- it { is_expected.to match_array(expected_errors) }
+ it_behaves_like 'report with expected errors'
end
end
@@ -331,16 +312,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
- context 'and the report passes schema validation' do
- let(:report_data) do
- {
- 'version' => '10.0.0',
- 'vulnerabilities' => []
- }
- end
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'report is valid with no error'
context 'and the report does not pass schema validation' do
let(:report_data) do
@@ -356,7 +328,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
]
end
- it { is_expected.to match_array(expected_errors) }
+ it_behaves_like 'report with expected errors'
end
end
@@ -383,7 +355,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
]
end
- it { is_expected.to match_array(expected_errors) }
+ it_behaves_like 'report with expected errors'
end
context 'and the report is invalid' do
@@ -400,7 +372,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
]
end
- it { is_expected.to match_array(expected_errors) }
+ it_behaves_like 'report with expected errors'
end
end
@@ -426,10 +398,27 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
]
end
- it { is_expected.to match_array(expected_errors) }
+ it_behaves_like 'report with expected errors'
end
end
+ shared_examples 'report is valid with no warning' do
+ context 'and the report is valid' do
+ let(:report_data) do
+ {
+ 'version' => report_version,
+ 'vulnerabilities' => []
+ }
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ shared_examples 'report with expected warnings' do
+ it { is_expected.to match_array(expected_deprecation_warnings) }
+ end
+
describe '#deprecation_warnings' do
subject { validator.deprecation_warnings }
@@ -491,7 +480,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- it { is_expected.to match_array(expected_deprecation_warnings) }
+ it_behaves_like 'report with expected warnings'
end
context 'and the report does not pass schema validation' do
@@ -501,7 +490,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- it { is_expected.to match_array(expected_deprecation_warnings) }
+ it_behaves_like 'report with expected warnings'
end
end
@@ -516,7 +505,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- it { is_expected.to match_array(expected_deprecation_warnings) }
+ it_behaves_like 'report with expected warnings'
end
end
@@ -561,21 +550,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
+ let(:security_report_failure) { 'schema_validation_fails' }
+
it { is_expected.to match_array([message]) }
- it 'logs related information' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "security report schema validation problem",
- security_report_type: report_type,
- security_report_version: report_version,
- project_id: project.id,
- security_report_failure: 'schema_validation_fails',
- security_report_scanner_id: 'gemnasium',
- security_report_scanner_version: '2.1.0'
- )
-
- subject
- end
+ it_behaves_like 'logs related information'
end
end
@@ -583,16 +562,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- context 'and the report is valid' do
- let(:report_data) do
- {
- 'version' => report_version,
- 'vulnerabilities' => []
- }
- end
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'report is valid with no warning'
context 'and the report is invalid' do
let(:report_data) do
@@ -644,16 +614,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { "12.37.0" }
- context 'and the report is valid' do
- let(:report_data) do
- {
- 'version' => report_version,
- 'vulnerabilities' => []
- }
- end
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'report is valid with no warning'
context 'and the report is invalid' do
let(:report_data) do
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 82fa11d5f98..821a5057d2e 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -4,11 +4,13 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
describe '#parse!' do
- subject { described_class.new.parse!(junit, test_suite, job: job) }
+ subject { described_class.new.parse!(junit, test_report, job: job) }
- let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') }
+ let(:job) { double(test_suite_name: 'rspec', max_test_cases_per_report: max_test_cases) }
+
+ let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
+ let(:test_suite) { test_report.get_suite(job.test_suite_name) }
let(:test_cases) { flattened_test_cases(test_suite) }
- let(:job) { double(max_test_cases_per_report: max_test_cases) }
let(:max_test_cases) { 0 }
context 'when data is JUnit style XML' do
diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb
index c9891c06507..a9adff4fce3 100644
--- a/spec/lib/gitlab/ci/parsers_spec.rb
+++ b/spec/lib/gitlab/ci/parsers_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers do
describe '.fabricate!' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb
new file mode 100644
index 00000000000..15df5b2f68c
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Chain::AssignPartition do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
+ end
+
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+ let(:step) { described_class.new(pipeline, command) }
+ let(:current_partition_id) { 123 }
+
+ describe '#perform!' do
+ before do
+ allow(Ci::Pipeline).to receive(:current_partition_value) { current_partition_id }
+ end
+
+ subject { step.perform! }
+
+ it 'assigns partition_id to pipeline' do
+ expect { subject }.to change(pipeline, :partition_id).to(current_partition_id)
+ end
+
+ context 'with parent-child pipelines' do
+ let(:bridge) do
+ instance_double(Ci::Bridge,
+ triggers_child_pipeline?: true,
+ parent_pipeline: instance_double(Ci::Pipeline, partition_id: 125))
+ end
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ bridge: bridge)
+ end
+
+ it 'assigns partition_id to pipeline' do
+ expect { subject }.to change(pipeline, :partition_id).to(125)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index de43e759193..6e8b6e40928 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -302,13 +302,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
context 'when bridge is present' do
context 'when bridge triggers a child pipeline' do
- let(:bridge) { double(:bridge, triggers_child_pipeline?: true) }
+ let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: true) }
it { is_expected.to be_truthy }
end
context 'when bridge triggers a multi-project pipeline' do
- let(:bridge) { double(:bridge, triggers_child_pipeline?: false) }
+ let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: false) }
it { is_expected.to be_falsey }
end
@@ -321,6 +321,38 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
+ describe '#parent_pipeline_partition_id' do
+ let(:command) { described_class.new(bridge: bridge) }
+
+ subject { command.parent_pipeline_partition_id }
+
+ context 'when bridge is present' do
+ context 'when bridge triggers a child pipeline' do
+ let(:pipeline) { instance_double(Ci::Pipeline, partition_id: 123) }
+
+ let(:bridge) do
+ instance_double(Ci::Bridge,
+ triggers_child_pipeline?: true,
+ parent_pipeline: pipeline)
+ end
+
+ it { is_expected.to eq(123) }
+ end
+
+ context 'when bridge triggers a multi-project pipeline' do
+ let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: false) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when bridge is not present' do
+ let(:bridge) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#increment_pipeline_failure_reason_counter' do
let(:command) { described_class.new }
let(:reason) { :size_limit_exceeded }
@@ -345,7 +377,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
describe '#observe_step_duration' do
context 'when ci_pipeline_creation_step_duration_tracking is enabled' do
it 'adds the duration to the step duration histogram' do
- histogram = double(:histogram)
+ histogram = instance_double(Prometheus::Client::Histogram)
duration = 1.hour
expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_step_duration_histogram)
diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
index e0d656f456e..f451bd6bfef 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
@@ -11,7 +11,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
subject { described_class.new(pipeline, command) }
- describe '#perform!' do
+ # TODO: change this to `describe` and remove rubocop-disable
+ # when removing the FF ci_project_pipeline_config_refactoring
+ shared_context '#perform!' do # rubocop:disable RSpec/ContextWording
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
@@ -201,4 +203,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
end
+
+ it_behaves_like '#perform!'
+
+ context 'when the FF ci_project_pipeline_config_refactoring is disabled' do
+ before do
+ stub_feature_flags(ci_project_pipeline_config_refactoring: false)
+ end
+
+ it_behaves_like '#perform!'
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
index e07a3ca9033..7fb5b0b4200 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
@@ -2,11 +2,13 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
+RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failures do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:stage) { build(:ci_stage, project: project, statuses: [job]) }
let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:environment) { project.environments.find_by_name('review/master') }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
@@ -24,12 +26,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
context 'when a pipeline contains a deployment job' do
let!(:job) { build(:ci_build, :start_review_app, project: project) }
- it 'ensures environment existence for the job' do
- expect { subject }.to change { Environment.count }.by(1)
+ context 'and the environment does not exist' do
+ it 'creates the environment specified by the job' do
+ expect { subject }.to change { Environment.count }.by(1)
- expect(project.environments.find_by_name('review/master')).to be_present
- expect(job.persisted_environment.name).to eq('review/master')
- expect(job.metadata.expanded_environment_name).to eq('review/master')
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request)
+ end
+
+ it 'associates the environment with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment.merge_request).to eq(merge_request)
+ end
+ end
end
context 'when an environment has already been existed' do
@@ -40,10 +56,22 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
it 'ensures environment existence for the job' do
expect { subject }.not_to change { Environment.count }
- expect(project.environments.find_by_name('review/master')).to be_present
+ expect(environment).to be_present
expect(job.persisted_environment.name).to eq('review/master')
expect(job.metadata.expanded_environment_name).to eq('review/master')
end
+
+ context 'and the pipeline is for a merge request' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request)
+ end
+
+ it 'does not associate the environment with the merge request' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment.merge_request).to be_nil
+ end
+ end
end
context 'when an environment name contains an invalid character' do
@@ -65,7 +93,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
it 'ensures environment existence for the job' do
expect { subject }.to change { Environment.count }.by(1)
- expect(project.environments.find_by_name('review/master')).to be_present
+ expect(environment).to be_present
expect(job.persisted_environment.name).to eq('review/master')
expect(job.metadata.expanded_environment_name).to eq('review/master')
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
index f7774e199fb..8c4f7af0ef4 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
@@ -277,7 +277,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
non_handled_sql_queries = 2
# 1. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache`
- # 2. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore`
extra_jobs * non_handled_sql_queries
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index e8eb3333b88..ee32661f267 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -83,19 +83,36 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do
.with({ source: 'push' }, 0)
end
- it 'records active jobs by pipeline plan in a histogram' do
- allow(command.metrics)
- .to receive(:active_jobs_histogram)
- .and_return(histogram)
+ describe 'active jobs by pipeline plan histogram' do
+ before do
+ allow(command.metrics)
+ .to receive(:active_jobs_histogram)
+ .and_return(histogram)
+
+ pipeline = create(:ci_pipeline, :running, project: project)
+ create_list(:ci_build, 3, pipeline: pipeline)
+ create(:ci_bridge, pipeline: pipeline)
+ end
- pipeline = create(:ci_pipeline, project: project, status: :running)
- create(:ci_build, :finished, project: project, pipeline: pipeline)
- create(:ci_build, :failed, project: project, pipeline: pipeline)
- create(:ci_build, :running, project: project, pipeline: pipeline)
- subject.build!
+ it 'counts all the active jobs' do
+ subject.build!
- expect(histogram).to have_received(:observe)
- .with(hash_including(plan: project.actual_plan_name), 3)
+ expect(histogram).to have_received(:observe)
+ .with(hash_including(plan: project.actual_plan_name), 4)
+ end
+
+ context 'when feature flag ci_limit_active_jobs_early is disabled' do
+ before do
+ stub_feature_flags(ci_limit_active_jobs_early: false)
+ end
+
+ it 'counts all the active builds' do
+ subject.build!
+
+ expect(histogram).to have_received(:observe)
+ .with(hash_including(plan: project.actual_plan_name), 3)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index fb1a360a4b7..52a00e0d501 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -179,6 +179,70 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
perform!
end
end
+
+ describe 'credit_card' do
+ context 'with no registered credit_card' do
+ it 'returns the expected credit card counts' do
+ expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
+ payload = Gitlab::Json.parse(params[:body])
+
+ expect(payload['credit_card']['similar_cards_count']).to eq(0)
+ expect(payload['credit_card']['similar_holder_names_count']).to eq(0)
+ end
+
+ perform!
+ end
+ end
+
+ context 'with a registered credit card' do
+ let!(:credit_card) { create(:credit_card_validation, last_digits: 10, holder_name: 'Alice', user: user) }
+
+ it 'returns the expected credit card counts' do
+ expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
+ payload = Gitlab::Json.parse(params[:body])
+
+ expect(payload['credit_card']['similar_cards_count']).to eq(1)
+ expect(payload['credit_card']['similar_holder_names_count']).to eq(1)
+ end
+
+ perform!
+ end
+
+ context 'with similar credit cards registered by other users' do
+ before do
+ create(:credit_card_validation, last_digits: 10, holder_name: 'Bob')
+ end
+
+ it 'returns the expected credit card counts' do
+ expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
+ payload = Gitlab::Json.parse(params[:body])
+
+ expect(payload['credit_card']['similar_cards_count']).to eq(2)
+ expect(payload['credit_card']['similar_holder_names_count']).to eq(1)
+ end
+
+ perform!
+ end
+ end
+
+ context 'with similar holder names registered by other users' do
+ before do
+ create(:credit_card_validation, last_digits: 11, holder_name: 'Alice')
+ end
+
+ it 'returns the expected credit card counts' do
+ expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
+ payload = Gitlab::Json.parse(params[:body])
+
+ expect(payload['credit_card']['similar_cards_count']).to eq(1)
+ expect(payload['credit_card']['similar_holder_names_count']).to eq(2)
+ end
+
+ perform!
+ end
+ end
+ end
+ end
end
context 'when EXTERNAL_VALIDATION_SERVICE_TOKEN is set' do
diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
index e0b4928d7f7..46c7072ad8e 100644
--- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Duration do
let(:calculated_duration) { calculate(data) }
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb
index 49686d1a9bd..3ca6fd9143f 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Null do
describe '.build' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
index c6d0d2534a5..b224fca6011 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
describe '.build' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb
index 3e10ca686ba..b030bd22aa1 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Variable do
describe '.build' do
diff --git a/spec/lib/gitlab/ci/pipeline/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/metrics_spec.rb
index 83b969ff3c4..8df3c67beaa 100644
--- a/spec/lib/gitlab/ci/pipeline/metrics_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/metrics_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe ::Gitlab::Ci::Pipeline::Metrics do
describe '.pipeline_creation_step_duration_histogram' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 890ba51157a..75f6a773c2d 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -97,15 +97,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:attributes) do
{ name: 'rspec',
ref: 'master',
- job_variables: [{ key: 'VAR1', value: 'var 1', public: true },
- { key: 'VAR2', value: 'var 2', public: true }],
+ job_variables: [{ key: 'VAR1', value: 'var 1' },
+ { key: 'VAR2', value: 'var 2' }],
rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] }
end
it do
- is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true },
- { key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }])
+ is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' },
+ { key: 'VAR3', value: 'var 3' },
+ { key: 'VAR2', value: 'var 2' }])
end
end
@@ -114,13 +114,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
{
name: 'rspec',
ref: 'master',
- job_variables: [{ key: 'VARIABLE', value: 'value', public: true }],
+ job_variables: [{ key: 'VARIABLE', value: 'value' }],
tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE']
}
end
it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) }
- it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value', public: true }]) }
+ it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) }
end
context 'with cache:key' do
@@ -257,19 +257,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:attributes) do
{ name: 'rspec',
ref: 'master',
- yaml_variables: [{ key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }],
- job_variables: [{ key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }],
+ yaml_variables: [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }],
+ job_variables: [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }],
root_variables_inheritance: root_variables_inheritance }
end
context 'when the pipeline has variables' do
let(:root_variables) do
- [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true },
- { key: 'VAR2', value: 'var pipeline 2', public: true },
- { key: 'VAR3', value: 'var pipeline 3', public: true },
- { key: 'VAR4', value: 'new var pipeline 4', public: true }]
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var pipeline 2' },
+ { key: 'VAR3', value: 'var pipeline 3' },
+ { key: 'VAR4', value: 'new var pipeline 4' }]
end
context 'when root_variables_inheritance is true' do
@@ -277,10 +277,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true },
- { key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true },
- { key: 'VAR4', value: 'new var pipeline 4', public: true }]
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' },
+ { key: 'VAR4', value: 'new var pipeline 4' }]
)
end
end
@@ -290,8 +290,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns job variables' do
expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }]
+ [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }]
)
end
end
@@ -301,9 +301,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true },
- { key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }]
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }]
)
end
end
@@ -314,8 +314,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns seed yaml variables' do
expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }])
+ [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }])
end
end
end
@@ -324,8 +324,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:attributes) do
{ name: 'rspec',
ref: 'master',
- yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }],
- job_variables: [{ key: 'VAR1', value: 'var 1', public: true }],
+ yaml_variables: [{ key: 'VAR1', value: 'var 1' }],
+ job_variables: [{ key: 'VAR1', value: 'var 1' }],
root_variables_inheritance: root_variables_inheritance,
rules: rules }
end
@@ -338,14 +338,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
it 'recalculates the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true },
- { key: 'VAR2', value: 'new var 2', public: true })
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
+ { key: 'VAR2', value: 'new var 2' })
end
end
context 'when the rules use root variables' do
let(:root_variables) do
- [{ key: 'VAR2', value: 'var pipeline 2', public: true }]
+ [{ key: 'VAR2', value: 'var pipeline 2' }]
end
let(:rules) do
@@ -353,15 +353,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
it 'recalculates the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true },
- { key: 'VAR2', value: 'overridden var 2', public: true })
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
+ { key: 'VAR2', value: 'overridden var 2' })
end
context 'when the root_variables_inheritance is false' do
let(:root_variables_inheritance) { false }
it 'does not recalculate the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1', public: true })
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' })
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
index 51185be3e74..6569ce937ac 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
@@ -6,8 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do
let_it_be(:project, refind: true) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
+ create(:ci_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
end
let(:job) { build(:ci_build, project: project, pipeline: pipeline) }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
index ad89f1f5cda..2b9d8127886 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
@@ -5,7 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
let_it_be(:project) { create(:project) }
- let(:job) { build(:ci_build, project: project) }
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:job) { build(:ci_build, project: project, pipeline: pipeline) }
let(:seed) { described_class.new(job) }
let(:attributes) { {} }
@@ -87,6 +89,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
it_behaves_like 'returning a correct environment'
end
+
+ context 'and job environment has an auto_stop_in variable attribute' do
+ let(:environment_auto_stop_in) { '10 minutes' }
+ let(:expected_auto_stop_in) { '10 minutes' }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: {
+ environment: {
+ name: environment_name,
+ auto_stop_in: '$TTL'
+ }
+ },
+ yaml_variables: [
+ { key: "TTL", value: environment_auto_stop_in, public: true }
+ ]
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
end
context 'when job has deployment tier attribute' do
@@ -167,5 +191,34 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
it_behaves_like 'returning a correct environment'
end
+
+ context 'when merge_request is provided' do
+ let(:environment_name) { 'development' }
+ let(:attributes) { { environment: environment_name, options: { environment: { name: environment_name } } } }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:seed) { described_class.new(job, merge_request: merge_request) }
+
+ context 'and environment does not exist' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+
+ it 'creates an environment associated with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject.merge_request).to eq(merge_request)
+ end
+ end
+
+ context 'and environment already exists' do
+ before do
+ create(:environment, project: project, name: environment_name)
+ end
+
+ it 'does not change the merge request associated with the environment' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(subject.merge_request).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb b/spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb
new file mode 100644
index 00000000000..a844ce6486b
--- /dev/null
+++ b/spec/lib/gitlab/ci/processable_object_hierarchy_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::ProcessableObjectHierarchy do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master') }
+
+ let_it_be(:job1) { create(:ci_build, :created, pipeline: pipeline, name: 'job1') }
+ let_it_be(:job2) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job2', needed: job1) }
+ let_it_be(:job3) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job3', needed: job1) }
+ let_it_be(:job4) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job4', needed: job2) }
+ let_it_be(:job5) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job5', needed: job3) }
+ let_it_be(:job6) { create(:ci_build, :created, :dependent, pipeline: pipeline, name: 'job6', needed: job4) }
+
+ describe '#base_and_ancestors' do
+ it 'includes the base and its ancestors' do
+ relation = described_class.new(::Ci::Processable.where(id: job2.id)).base_and_ancestors
+
+ expect(relation).to eq([job2, job1])
+ end
+
+ it 'can find ancestors upto a certain level' do
+ relation = described_class.new(::Ci::Processable.where(id: job4.id)).base_and_ancestors(upto: job1.name)
+
+ expect(relation).to eq([job4, job2])
+ end
+
+ describe 'hierarchy_order option' do
+ let(:relation) do
+ described_class.new(::Ci::Processable.where(id: job4.id)).base_and_ancestors(hierarchy_order: hierarchy_order)
+ end
+
+ context 'for :asc' do
+ let(:hierarchy_order) { :asc }
+
+ it 'orders by child to ancestor' do
+ expect(relation).to eq([job4, job2, job1])
+ end
+ end
+
+ context 'for :desc' do
+ let(:hierarchy_order) { :desc }
+
+ it 'orders by ancestor to child' do
+ expect(relation).to eq([job1, job2, job4])
+ end
+ end
+ end
+ end
+
+ describe '#base_and_descendants' do
+ it 'includes the base and its descendants' do
+ relation = described_class.new(::Ci::Processable.where(id: job2.id)).base_and_descendants
+
+ expect(relation).to contain_exactly(job2, job4, job6)
+ end
+
+ context 'when with_depth is true' do
+ let(:relation) do
+ described_class.new(::Ci::Processable.where(id: job1.id)).base_and_descendants(with_depth: true)
+ end
+
+ it 'includes depth in the results' do
+ object_depths = {
+ job1.id => 1,
+ job2.id => 2,
+ job3.id => 2,
+ job4.id => 3,
+ job5.id => 3,
+ job6.id => 4
+ }
+
+ relation.each do |object|
+ expect(object.depth).to eq(object_depths[object.id])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/project_config/repository_spec.rb b/spec/lib/gitlab/ci/project_config/repository_spec.rb
new file mode 100644
index 00000000000..2105b691d9e
--- /dev/null
+++ b/spec/lib/gitlab/ci/project_config/repository_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::ProjectConfig::Repository do
+ let(:project) { create(:project, :custom_repo, files: files) }
+ let(:sha) { project.repository.head_commit.sha }
+ let(:files) { { 'README.md' => 'hello' } }
+
+ subject(:config) { described_class.new(project, sha, nil, nil, nil) }
+
+ describe '#content' do
+ subject(:content) { config.content }
+
+ context 'when file is in repository' do
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - local: ".gitlab-ci.yml"
+ CICONFIG
+ end
+
+ let(:files) { { '.gitlab-ci.yml' => 'content' } }
+
+ it { is_expected.to eq(config_content_result) }
+ end
+
+ context 'when file is not in repository' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when Gitaly raises error' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml_for).and_raise(GRPC::Internal)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#source' do
+ subject { config.source }
+
+ it { is_expected.to eq(:repository_source) }
+ end
+end
diff --git a/spec/lib/gitlab/ci/project_config/source_spec.rb b/spec/lib/gitlab/ci/project_config/source_spec.rb
new file mode 100644
index 00000000000..dda5c7cdce8
--- /dev/null
+++ b/spec/lib/gitlab/ci/project_config/source_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::ProjectConfig::Source do
+ let_it_be(:custom_config_class) { Class.new(described_class) }
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:sha) { '123456' }
+
+ subject(:custom_config) { custom_config_class.new(project, sha, nil, nil, nil) }
+
+ describe '#content' do
+ subject(:content) { custom_config.content }
+
+ it { expect { content }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#source' do
+ subject(:source) { custom_config.source }
+
+ it { expect { source }.to raise_error(NotImplementedError) }
+ end
+end
diff --git a/spec/lib/gitlab/ci/project_config_spec.rb b/spec/lib/gitlab/ci/project_config_spec.rb
new file mode 100644
index 00000000000..c4b179c9ef5
--- /dev/null
+++ b/spec/lib/gitlab/ci/project_config_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::ProjectConfig do
+ let(:project) { create(:project, :empty_repo, ci_config_path: ci_config_path) }
+ let(:sha) { '123456' }
+ let(:content) { nil }
+ let(:source) { :push }
+ let(:bridge) { nil }
+
+ subject(:config) do
+ described_class.new(project: project, sha: sha,
+ custom_content: content, pipeline_source: source, pipeline_source_bridge: bridge)
+ end
+
+ context 'when bridge job is passed in as parameter' do
+ let(:ci_config_path) { nil }
+ let(:bridge) { create(:ci_bridge) }
+
+ before do
+ allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
+ end
+
+ it 'returns the content already available in command' do
+ expect(config.source).to eq(:bridge_source)
+ expect(config.content).to eq('the-yaml')
+ end
+ end
+
+ context 'when config is defined in a custom path in the repository' do
+ let(:ci_config_path) { 'path/to/config.yml' }
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - local: #{ci_config_path}
+ CICONFIG
+ end
+
+ before do
+ allow(project.repository)
+ .to receive(:gitlab_ci_yml_for)
+ .with(sha, ci_config_path)
+ .and_return('the-content')
+ end
+
+ it 'returns root config including the local custom file' do
+ expect(config.source).to eq(:repository_source)
+ expect(config.content).to eq(config_content_result)
+ end
+ end
+
+ context 'when config is defined remotely' do
+ let(:ci_config_path) { 'http://example.com/path/to/ci/config.yml' }
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - remote: #{ci_config_path}
+ CICONFIG
+ end
+
+ it 'returns root config including the remote config' do
+ expect(config.source).to eq(:remote_source)
+ expect(config.content).to eq(config_content_result)
+ end
+ end
+
+ context 'when config is defined in a separate repository' do
+ let(:ci_config_path) { 'path/to/.gitlab-ci.yml@another-group/another-repo' }
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - project: another-group/another-repo
+ file: path/to/.gitlab-ci.yml
+ CICONFIG
+ end
+
+ it 'returns root config including the path to another repository' do
+ expect(config.source).to eq(:external_project_source)
+ expect(config.content).to eq(config_content_result)
+ end
+
+ context 'when path specifies a refname' do
+ let(:ci_config_path) { 'path/to/.gitlab-ci.yml@another-group/another-repo:refname' }
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - project: another-group/another-repo
+ file: path/to/.gitlab-ci.yml
+ ref: refname
+ CICONFIG
+ end
+
+ it 'returns root config including the path and refname to another repository' do
+ expect(config.source).to eq(:external_project_source)
+ expect(config.content).to eq(config_content_result)
+ end
+ end
+ end
+
+ context 'when config is defined in the default .gitlab-ci.yml' do
+ let(:ci_config_path) { nil }
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - local: ".gitlab-ci.yml"
+ CICONFIG
+ end
+
+ before do
+ allow(project.repository)
+ .to receive(:gitlab_ci_yml_for)
+ .with(sha, '.gitlab-ci.yml')
+ .and_return('the-content')
+ end
+
+ it 'returns root config including the canonical CI config file' do
+ expect(config.source).to eq(:repository_source)
+ expect(config.content).to eq(config_content_result)
+ end
+ end
+
+ context 'when config is the Auto-Devops template' do
+ let(:ci_config_path) { nil }
+ let(:config_content_result) do
+ <<~CICONFIG
+ ---
+ include:
+ - template: Auto-DevOps.gitlab-ci.yml
+ CICONFIG
+ end
+
+ before do
+ allow(project).to receive(:auto_devops_enabled?).and_return(true)
+ end
+
+ it 'returns root config including the auto-devops template' do
+ expect(config.source).to eq(:auto_devops_source)
+ expect(config.content).to eq(config_content_result)
+ end
+ end
+
+ context 'when config is passed as a parameter' do
+ let(:source) { :ondemand_dast_scan }
+ let(:ci_config_path) { nil }
+ let(:content) do
+ <<~CICONFIG
+ ---
+ stages:
+ - dast
+ CICONFIG
+ end
+
+ it 'returns the parameter content' do
+ expect(config.source).to eq(:parameter_source)
+ expect(config.content).to eq(content)
+ end
+ end
+
+ context 'when config is not defined anywhere' do
+ let(:ci_config_path) { nil }
+
+ before do
+ allow(project).to receive(:auto_devops_enabled?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(config.source).to be_nil
+ expect(config.content).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
index ade0e36cf1e..ad8f1dc11f8 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
let(:comparer) { described_class.new(base_report, head_report) }
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
index 8c35b2a34cf..af6844491ca 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
let(:accessibility_report) { described_class.new }
diff --git a/spec/lib/gitlab/ci/reports/coverage_report_spec.rb b/spec/lib/gitlab/ci/reports/coverage_report_spec.rb
index 53646f7dfc0..23361a0c768 100644
--- a/spec/lib/gitlab/ci/reports/coverage_report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/coverage_report_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::CoverageReport do
let(:coverage_report) { described_class.new }
diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
index 672117c311f..06ea3433ef0 100644
--- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Sbom::Component do
let(:attributes) do
{
- 'type' => 'library',
- 'name' => 'component-name',
- 'version' => 'v0.0.1'
+ type: 'library',
+ name: 'component-name',
+ version: 'v0.0.1'
}
end
- subject { described_class.new(attributes) }
+ subject { described_class.new(**attributes) }
it 'has correct attributes' do
expect(subject).to have_attributes(
- component_type: 'library',
- name: 'component-name',
- version: 'v0.0.1'
+ component_type: attributes[:type],
+ name: attributes[:name],
+ version: attributes[:version]
)
end
end
diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
index d7a285ab13c..6ffa93e5fc8 100644
--- a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
@@ -15,40 +15,22 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Report do
end
describe '#set_source' do
- let_it_be(:source) do
- {
- 'type' => :dependency_scanning,
- 'data' => {
- 'input_file' => { 'path' => 'package-lock.json' },
- 'source_file' => { 'path' => 'package.json' },
- 'package_manager' => { 'name' => 'npm' },
- 'language' => { 'name' => 'JavaScript' }
- },
- 'fingerprint' => 'c01df1dc736c1148717e053edbde56cb3a55d3e31f87cea955945b6f67c17d42'
- }
- end
+ let_it_be(:source) { create(:ci_reports_sbom_source) }
it 'stores the source' do
report.set_source(source)
- expect(report.source).to be_a(Gitlab::Ci::Reports::Sbom::Source)
+ expect(report.source).to eq(source)
end
end
describe '#add_component' do
- let_it_be(:components) do
- [
- { 'type' => 'library', 'name' => 'component1', 'version' => 'v0.0.1' },
- { 'type' => 'library', 'name' => 'component2', 'version' => 'v0.0.2' },
- { 'type' => 'library', 'name' => 'component2' }
- ]
- end
+ let_it_be(:components) { create_list(:ci_reports_sbom_component, 3) }
it 'appends components to a list' do
components.each { |component| report.add_component(component) }
- expect(report.components.size).to eq(3)
- expect(report.components).to all(be_a(Gitlab::Ci::Reports::Sbom::Component))
+ expect(report.components).to match_array(components)
end
end
end
diff --git a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb
index 97d8d7abb33..75ea91251eb 100644
--- a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Sbom::Reports do
subject(:reports_list) { described_class.new }
diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
index 2d6434534a0..cb30bd721dd 100644
--- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
@@ -1,29 +1,29 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Sbom::Source do
let(:attributes) do
{
- 'type' => :dependency_scanning,
- 'data' => {
+ type: :dependency_scanning,
+ data: {
'category' => 'development',
'input_file' => { 'path' => 'package-lock.json' },
'source_file' => { 'path' => 'package.json' },
'package_manager' => { 'name' => 'npm' },
'language' => { 'name' => 'JavaScript' }
},
- 'fingerprint' => '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188'
+ fingerprint: '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188'
}
end
- subject { described_class.new(attributes) }
+ subject { described_class.new(**attributes) }
it 'has correct attributes' do
expect(subject).to have_attributes(
- source_type: attributes['type'],
- data: attributes['data'],
- fingerprint: attributes['fingerprint']
+ source_type: attributes[:type],
+ data: attributes[:data],
+ fingerprint: attributes[:fingerprint]
)
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/flag_spec.rb b/spec/lib/gitlab/ci/reports/security/flag_spec.rb
index d677425a8da..6ee074f7aeb 100644
--- a/spec/lib/gitlab/ci/reports/security/flag_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/flag_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Flag do
subject(:security_flag) { described_class.new(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink') }
diff --git a/spec/lib/gitlab/ci/reports/security/link_spec.rb b/spec/lib/gitlab/ci/reports/security/link_spec.rb
index 7b55af27f4d..0e1cdc93f6c 100644
--- a/spec/lib/gitlab/ci/reports/security/link_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/link_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Link do
subject(:security_link) { described_class.new(name: 'CVE-2020-0202', url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202') }
diff --git a/spec/lib/gitlab/ci/reports/security/scan_spec.rb b/spec/lib/gitlab/ci/reports/security/scan_spec.rb
index b4968ff3a6e..23427e8608c 100644
--- a/spec/lib/gitlab/ci/reports/security/scan_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/scan_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Scan do
describe '#initialize' do
diff --git a/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb b/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb
index e9daa05e8b9..74a5344f79e 100644
--- a/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::ScannedResource do
let(:url) { 'http://example.com:3001/1?foo=bar' }
diff --git a/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb
index 5e94fe2bb3d..f754786d071 100644
--- a/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Reports::TerraformReports do
it 'initializes plans with and empty hash' do
diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb
index 3e1004754ba..e81c7b0f6be 100644
--- a/spec/lib/gitlab/ci/status/extended_spec.rb
+++ b/spec/lib/gitlab/ci/status/extended_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Status::Extended do
it 'requires subclass to implement matcher' do
diff --git a/spec/lib/gitlab/ci/status/group/factory_spec.rb b/spec/lib/gitlab/ci/status/group/factory_spec.rb
index c67c7ff8271..38aa1ba4ebb 100644
--- a/spec/lib/gitlab/ci/status/group/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/group/factory_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Status::Group::Factory do
it 'inherits from the core factory' do
diff --git a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..5a62324da74
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Katalon.gitlab-ci.yml' do
+ subject(:template) do
+ <<~YAML
+ include:
+ - template: 'Katalon.gitlab-ci.yml'
+
+ katalon_tests_placeholder:
+ extends: .katalon_tests
+ stage: test
+ script:
+ - echo "katalon tests"
+
+ katalon_tests_with_artifacts_placeholder:
+ extends: .katalon_tests_with_artifacts
+ stage: test
+ script:
+ - echo "katalon tests with artifacts"
+ YAML
+ end
+
+ describe 'the created pipeline' do
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:user) { project.first_owner }
+
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template)
+ end
+
+ it 'create katalon tests jobs' do
+ expect(build_names).to match_array(%w[katalon_tests_placeholder katalon_tests_with_artifacts_placeholder])
+
+ expect(pipeline.builds.find_by(name: 'katalon_tests_placeholder').options).to include(
+ image: { name: 'katalonstudio/katalon' },
+ services: [{ name: 'docker:dind' }]
+ )
+
+ expect(pipeline.builds.find_by(name: 'katalon_tests_with_artifacts_placeholder').options).to include(
+ image: { name: 'katalonstudio/katalon' },
+ services: [{ name: 'docker:dind' }],
+ artifacts: { when: 'always', paths: ['Reports/'], reports: { junit: ['Reports/*/*/*/*.xml'] } }
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb
index 3ae0e5d1f0e..f91cb03883a 100644
--- a/spec/lib/gitlab/ci/trace/archive_spec.rb
+++ b/spec/lib/gitlab/ci/trace/archive_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Trace::Archive do
context 'with transactional fixtures' do
- let_it_be(:job) { create(:ci_build, :success, :trace_live) }
+ let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live) }
let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) }
let_it_be(:src_checksum) do
job.trace.read { |stream| Digest::MD5.hexdigest(stream.raw) }
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 888ceb7ff9a..3043c8c5467 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa
describe "associations" do
it { expect(trace).to respond_to(:job) }
- it { expect(trace).to delegate_method(:old_trace).to(:job) }
end
context 'when trace is migrated to object storage' do
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index 6ab2089cce8..4833ccf9093 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Variables::Builder do
+RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
include Ci::TemplateHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, namespace: group) }
@@ -26,13 +26,13 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
{ key: 'CI_JOB_NAME',
value: job.name },
{ key: 'CI_JOB_STAGE',
- value: job.stage },
+ value: job.stage_name },
{ key: 'CI_NODE_TOTAL',
value: '1' },
{ key: 'CI_BUILD_NAME',
value: job.name },
{ key: 'CI_BUILD_STAGE',
- value: job.stage },
+ value: job.stage_name },
{ key: 'CI',
value: 'true' },
{ key: 'GITLAB_CI',
@@ -138,11 +138,11 @@ RSpec.describe Gitlab::Ci::Variables::Builder do
{ key: 'GITLAB_USER_ID',
value: user.id.to_s },
{ key: 'GITLAB_USER_EMAIL',
- value: user.email },
+ value: user.email },
{ key: 'GITLAB_USER_LOGIN',
- value: user.username },
+ value: user.username },
{ key: 'GITLAB_USER_NAME',
- value: user.name }
+ value: user.name }
].map { |var| var.merge(public: true, masked: false) }
end
diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
index 7e4e9602a92..57171e5be69 100644
--- a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
@@ -2,6 +2,7 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
+require 'tsort'
RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
describe '#initialize with non-Collection value' do
diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb
index fc1055751bd..fb1e66bd605 100644
--- a/spec/lib/gitlab/ci/variables/helpers_spec.rb
+++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb
@@ -15,21 +15,21 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do
end
let(:result) do
- [{ key: 'key1', value: 'value1', public: true },
- { key: 'key2', value: 'value22', public: true },
- { key: 'key3', value: 'value3', public: true }]
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value22' },
+ { key: 'key3', value: 'value3' }]
end
subject { described_class.merge_variables(current_variables, new_variables) }
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
context 'when new variables is a hash' do
let(:new_variables) do
{ 'key2' => 'value22', 'key3' => 'value3' }
end
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
end
context 'when new variables is a hash with symbol keys' do
@@ -37,79 +37,68 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do
{ key2: 'value22', key3: 'value3' }
end
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
end
context 'when new variables is nil' do
let(:new_variables) {}
let(:result) do
- [{ key: 'key1', value: 'value1', public: true },
- { key: 'key2', value: 'value2', public: true }]
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value2' }]
end
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
end
end
- describe '.transform_to_yaml_variables' do
- let(:variables) do
- { 'key1' => 'value1', 'key2' => 'value2' }
- end
-
- let(:result) do
- [{ key: 'key1', value: 'value1', public: true },
- { key: 'key2', value: 'value2', public: true }]
- end
-
- subject { described_class.transform_to_yaml_variables(variables) }
-
- it { is_expected.to eq(result) }
+ describe '.transform_to_array' do
+ subject { described_class.transform_to_array(variables) }
- context 'when variables is nil' do
- let(:variables) {}
-
- it { is_expected.to eq([]) }
- end
- end
+ context 'when values are strings' do
+ let(:variables) do
+ { 'key1' => 'value1', 'key2' => 'value2' }
+ end
- describe '.transform_from_yaml_variables' do
- let(:variables) do
- [{ key: 'key1', value: 'value1', public: true },
- { key: 'key2', value: 'value2', public: true }]
- end
+ let(:result) do
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value2' }]
+ end
- let(:result) do
- { 'key1' => 'value1', 'key2' => 'value2' }
+ it { is_expected.to match_array(result) }
end
- subject { described_class.transform_from_yaml_variables(variables) }
-
- it { is_expected.to eq(result) }
-
context 'when variables is nil' do
let(:variables) {}
- it { is_expected.to eq({}) }
+ it { is_expected.to match_array([]) }
end
- context 'when variables is a hash' do
+ context 'when values are hashes' do
let(:variables) do
- { key1: 'value1', 'key2' => 'value2' }
+ { 'key1' => { value: 'value1', description: 'var 1' }, 'key2' => { value: 'value2' } }
end
- it { is_expected.to eq(result) }
- end
-
- context 'when variables contain integers and symbols' do
- let(:variables) do
- { key1: 1, key2: :value2 }
+ let(:result) do
+ [{ key: 'key1', value: 'value1', description: 'var 1' },
+ { key: 'key2', value: 'value2' }]
end
- let(:result1) do
- { 'key1' => '1', 'key2' => 'value2' }
- end
+ it { is_expected.to match_array(result) }
+
+ context 'when a value data has `key` as a key' do
+ let(:variables) do
+ { 'key1' => { value: 'value1', key: 'new_key1' }, 'key2' => { value: 'value2' } }
+ end
+
+ let(:result) do
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value2' }]
+ end
- it { is_expected.to eq(result1) }
+ it 'ignores the key set with "key"' do
+ is_expected.to match_array(result)
+ end
+ end
end
end
@@ -127,35 +116,35 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do
let(:inheritance) { true }
let(:result) do
- [{ key: 'key1', value: 'value1', public: true },
- { key: 'key2', value: 'value22', public: true },
- { key: 'key3', value: 'value3', public: true }]
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value22' },
+ { key: 'key3', value: 'value3' }]
end
subject { described_class.inherit_yaml_variables(from: from, to: to, inheritance: inheritance) }
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
context 'when inheritance is false' do
let(:inheritance) { false }
let(:result) do
- [{ key: 'key2', value: 'value22', public: true },
- { key: 'key3', value: 'value3', public: true }]
+ [{ key: 'key2', value: 'value22' },
+ { key: 'key3', value: 'value3' }]
end
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
end
context 'when inheritance is array' do
let(:inheritance) { ['key2'] }
let(:result) do
- [{ key: 'key2', value: 'value22', public: true },
- { key: 'key3', value: 'value3', public: true }]
+ [{ key: 'key2', value: 'value22' },
+ { key: 'key3', value: 'value3' }]
end
- it { is_expected.to eq(result) }
+ it { is_expected.to match_array(result) }
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
index f815f56543c..082febacbd7 100644
--- a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tsort'
RSpec.describe Gitlab::Ci::YamlProcessor::Dag do
let(:nodes) { {} }
diff --git a/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb b/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb
index 0bd9563d191..77346f328ca 100644
--- a/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::YamlProcessor::FeatureFlags do
let(:feature_flag) { :my_feature_flag }
@@ -48,20 +48,32 @@ RSpec.describe Gitlab::Ci::YamlProcessor::FeatureFlags do
end
context 'when feature flag is checked outside the "with_actor" block' do
- it 'raises an error on dev/test environment' do
- expect { described_class.enabled?(feature_flag) }.to raise_error(described_class::NoActorError)
- end
+ context 'when yaml_processor_feature_flag_corectness is used', :yaml_processor_feature_flag_corectness do
+ it 'raises an error on dev/test environment' do
+ expect { described_class.enabled?(feature_flag) }.to raise_error(described_class::NoActorError)
+ end
+
+ context 'when on production' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
- context 'when on production' do
- before do
- allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ it 'checks the feature flag without actor' do
+ expect(Feature).to receive(:enabled?).with(feature_flag, nil)
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_and_raise_for_dev_exception)
+ .and_call_original
+
+ described_class.enabled?(feature_flag)
+ end
end
+ end
+ context 'when yaml_processor_feature_flag_corectness is not used' do
it 'checks the feature flag without actor' do
expect(Feature).to receive(:enabled?).with(feature_flag, nil)
expect(Gitlab::ErrorTracking)
- .to receive(:track_and_raise_for_dev_exception)
- .and_call_original
+ .to receive(:track_exception)
described_class.enabled?(feature_flag)
end
diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
index 8416501e949..f7a0905d9da 100644
--- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
@@ -72,8 +72,8 @@ module Gitlab
it 'returns calculated variables with root and job variables' do
is_expected.to match_array([
- { key: 'VAR1', value: 'value 11', public: true },
- { key: 'VAR2', value: 'value 2', public: true }
+ { key: 'VAR1', value: 'value 11' },
+ { key: 'VAR2', value: 'value 2' }
])
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 35af9ae6201..cc327f5b5f1 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -298,8 +298,8 @@ module Gitlab
context 'when delayed is defined' do
let(:config) do
YAML.dump(rspec: {
- script: 'rollout 10%',
- when: 'delayed',
+ script: 'rollout 10%',
+ when: 'delayed',
start_in: '1 day'
})
end
@@ -315,7 +315,7 @@ module Gitlab
context 'when resource group is defined' do
let(:config) do
YAML.dump(rspec: {
- script: 'test',
+ script: 'test',
resource_group: 'iOS'
})
end
@@ -448,7 +448,7 @@ module Gitlab
it 'parses the root:variables as #root_variables' do
expect(subject.root_variables)
- .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
+ .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' })
end
end
@@ -490,7 +490,7 @@ module Gitlab
it 'parses the root:variables as #root_variables' do
expect(subject.root_variables)
- .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
+ .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' })
end
end
@@ -997,18 +997,6 @@ module Gitlab
scheduling_type: :stage
})
end
-
- context 'when the feature flag ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it { is_expected.not_to be_valid }
-
- it "returns no job" do
- expect(processor.jobs).to eq({})
- end
- end
end
context 'when a service has pull_policy' do
@@ -1042,39 +1030,29 @@ module Gitlab
scheduling_type: :stage
})
end
-
- context 'when the feature flag ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it { is_expected.not_to be_valid }
-
- it "returns no job" do
- expect(processor.jobs).to eq({})
- end
- end
end
end
- describe 'Variables' do
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ # Change this to a `describe` block when removing the FF ci_variables_refactoring_to_variable
+ shared_examples 'Variables' do
+ subject(:execute) { described_class.new(config).execute }
- let(:build) { subject.builds.first }
+ let(:build) { execute.builds.first }
let(:job_variables) { build[:job_variables] }
let(:root_variables_inheritance) { build[:root_variables_inheritance] }
context 'when global variables are defined' do
- let(:variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
-
let(:config) do
- {
- variables: variables,
- before_script: ['pwd'],
- rspec: { script: 'rspec' }
- }
+ <<~YAML
+ variables:
+ VAR1: value1
+ VAR2: value2
+
+ before_script: [pwd]
+
+ rspec:
+ script: rspec
+ YAML
end
it 'returns global variables' do
@@ -1084,22 +1062,23 @@ module Gitlab
end
context 'when job variables are defined' do
- let(:config) do
- {
- before_script: ['pwd'],
- rspec: { script: 'rspec', variables: variables }
- }
- end
-
context 'when syntax is correct' do
- let(:variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ let(:config) do
+ <<~YAML
+ before_script: [pwd]
+
+ rspec:
+ script: rspec
+ variables:
+ VAR1: value1
+ VAR2: value2
+ YAML
end
it 'returns job variables' do
expect(job_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' }
)
expect(root_variables_inheritance).to eq(true)
end
@@ -1107,16 +1086,28 @@ module Gitlab
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
- let(:variables) do
- %w(VAR1 value1 VAR2 value2)
+ let(:config) do
+ <<~YAML
+ before_script: [pwd]
+
+ rspec:
+ script: rspec
+ variables: [VAR1 value1 VAR2 value2]
+ YAML
end
- it_behaves_like 'returns errors', /jobs:rspec:variables config should be a hash of key value pairs/
+ it_behaves_like 'returns errors', /jobs:rspec:variables config should be a hash/
end
context 'when variables key defined but value not specified' do
- let(:variables) do
- nil
+ let(:config) do
+ <<~YAML
+ before_script: [pwd]
+
+ rspec:
+ script: rspec
+ variables: null
+ YAML
end
it 'returns empty array' do
@@ -1133,10 +1124,12 @@ module Gitlab
context 'when job variables are not defined' do
let(:config) do
- {
- before_script: ['pwd'],
- rspec: { script: 'rspec' }
- }
+ <<~YAML
+ before_script: ['pwd']
+
+ rspec:
+ script: rspec
+ YAML
end
it 'returns empty array' do
@@ -1144,6 +1137,42 @@ module Gitlab
expect(root_variables_inheritance).to eq(true)
end
end
+
+ context 'when variables have different type of values' do
+ let(:config) do
+ <<~YAML
+ before_script: [pwd]
+
+ rspec:
+ variables:
+ VAR1: value1
+ VAR2: :value2
+ VAR3: 123
+ script: rspec
+ YAML
+ end
+
+ it 'returns job variables' do
+ expect(job_variables).to contain_exactly(
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' },
+ { key: 'VAR3', value: '123' }
+ )
+ expect(root_variables_inheritance).to eq(true)
+ end
+ end
+ end
+
+ context 'when ci_variables_refactoring_to_variable is enabled' do
+ it_behaves_like 'Variables'
+ end
+
+ context 'when ci_variables_refactoring_to_variable is disabled' do
+ before do
+ stub_feature_flags(ci_variables_refactoring_to_variable: false)
+ end
+
+ it_behaves_like 'Variables'
end
context 'when using `extends`' do
@@ -1203,21 +1232,21 @@ module Gitlab
expect(config_processor.builds[0]).to include(
name: 'test1',
options: { script: ['test'] },
- job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true },
- { key: 'VAR2', value: 'test2 var 2', public: true }]
+ job_variables: [{ key: 'VAR1', value: 'test1 var 1' },
+ { key: 'VAR2', value: 'test2 var 2' }]
)
expect(config_processor.builds[1]).to include(
name: 'test2',
options: { script: ['test'] },
- job_variables: [{ key: 'VAR1', value: 'base var 1', public: true },
- { key: 'VAR2', value: 'test2 var 2', public: true }]
+ job_variables: [{ key: 'VAR1', value: 'base var 1' },
+ { key: 'VAR2', value: 'test2 var 2' }]
)
expect(config_processor.builds[2]).to include(
name: 'test3',
options: { script: ['test'] },
- job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }]
+ job_variables: [{ key: 'VAR1', value: 'base var 1' }]
)
expect(config_processor.builds[3]).to include(
@@ -1647,10 +1676,10 @@ module Gitlab
describe "Artifacts" do
it "returns artifacts when defined" do
config = YAML.dump({
- image: "image:1.0",
- services: ["mysql"],
+ image: "image:1.0",
+ services: ["mysql"],
before_script: ["pwd"],
- rspec: {
+ rspec: {
artifacts: {
paths: ["logs/", "binaries/"],
expose_as: "Exposed artifacts",
@@ -1906,7 +1935,7 @@ module Gitlab
let(:config) do
{
deploy_to_production: {
- stage: 'deploy',
+ stage: 'deploy',
script: 'test'
}
}
@@ -2275,15 +2304,15 @@ module Gitlab
let(:config) do
{
- var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] },
- var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] },
+ var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] },
+ var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] },
var_and_changes: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', changes: %w[README], when: 'always' }] },
changes_not_var: { stage: 'test', script: 'test', rules: [{ if: '$VAR != null', changes: %w[README] }] },
var_not_changes: { stage: 'test', script: 'test', rules: [{ if: '$VAR == null', changes: %w[other/file.rb], when: 'always' }] },
- nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] },
- var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] },
- var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] },
- two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] }
+ nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] },
+ var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] },
+ var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] },
+ two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] }
}
end
@@ -2729,13 +2758,13 @@ module Gitlab
context 'returns errors if variables is not a map' do
let(:config) { YAML.dump({ variables: "test", rspec: { script: "test" } }) }
- it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash'
+ it_behaves_like 'returns errors', 'variables config should be a hash'
end
context 'returns errors if variables is not a map of key-value strings' do
let(:config) { YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) }
- it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash'
+ it_behaves_like 'returns errors', 'variable definition must be either a string or a hash'
end
context 'returns errors if job when is not on_success, on_failure or always' do
diff --git a/spec/lib/gitlab/ci_access_spec.rb b/spec/lib/gitlab/ci_access_spec.rb
index 9b573c6eb7a..e41b666abda 100644
--- a/spec/lib/gitlab/ci_access_spec.rb
+++ b/spec/lib/gitlab/ci_access_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::CiAccess do
let(:access) { described_class.new }
diff --git a/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb b/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..36c5d0e9b0c
--- /dev/null
+++ b/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Cleanup::PersonalAccessTokens do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+
+ let(:group_full_path) { group.full_path }
+ let(:logger) { instance_double(Gitlab::AppJsonLogger, info: nil, warn: nil) }
+ let(:last_used_at) { 1.month.ago.beginning_of_hour }
+ let!(:unused_token) { create(:personal_access_token) }
+
+ let!(:old_unused_token) do
+ create(:personal_access_token, created_at: last_used_at - 1.minute)
+ end
+
+ let!(:old_actively_used_token) do
+ create(:personal_access_token, created_at: last_used_at - 1.minute, last_used_at: 1.day.ago)
+ end
+
+ let!(:old_unused_token_for_non_group_member) do
+ create(:personal_access_token, created_at: last_used_at - 1.minute)
+ end
+
+ let!(:old_unused_token_for_subgroup_member) do
+ create(:personal_access_token, created_at: last_used_at - 1.minute)
+ end
+
+ let!(:old_unused_project_access_token) do
+ create(:personal_access_token, user: project_bot, created_at: last_used_at - 1.minute)
+ end
+
+ let!(:old_formerly_used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute,
+ last_used_at: last_used_at - 1.minute
+ )
+ end
+
+ before do
+ group.add_member(old_formerly_used_token.user, Gitlab::Access::DEVELOPER)
+ group.add_member(old_actively_used_token.user, Gitlab::Access::DEVELOPER)
+ group.add_member(unused_token.user, Gitlab::Access::DEVELOPER)
+ group.add_member(old_unused_token.user, Gitlab::Access::DEVELOPER)
+ group.add_member(project_bot, Gitlab::Access::MAINTAINER)
+
+ subgroup.add_member(old_unused_token_for_subgroup_member.user, Gitlab::Access::DEVELOPER)
+ end
+
+ subject do
+ described_class.new(
+ logger: logger,
+ cut_off_date: last_used_at,
+ group_full_path: group_full_path
+ )
+ end
+
+ context 'when initialized with an invalid logger' do
+ let(:logger) { "not a logger" }
+
+ it 'raises error' do
+ expect do
+ subject.run!
+ end.to raise_error('Invalid logger: not a logger')
+ end
+ end
+
+ describe '#run!' do
+ context 'when invalid group path passed' do
+ let(:group_full_path) { 'notagroup' }
+
+ it 'raises error' do
+ expect do
+ subject.run!
+ end.to raise_error("Group with full_path notagroup not found")
+ end
+ end
+
+ context 'in a real run' do
+ let(:args) { { dry_run: false } }
+
+ context 'when revoking unused tokens' do
+ it 'revokes human-owned tokens created and last used over 1 year ago' do
+ subject.run!(**args)
+
+ expect(PersonalAccessToken.active).to contain_exactly(
+ unused_token,
+ old_actively_used_token,
+ old_unused_project_access_token,
+ old_unused_token_for_non_group_member,
+ old_unused_token_for_subgroup_member
+ )
+ expect(PersonalAccessToken.revoked).to contain_exactly(
+ old_unused_token,
+ old_formerly_used_token
+ )
+ end
+ end
+
+ context 'when revoking used and unused tokens' do
+ let(:args) { { dry_run: false, revoke_active_tokens: true } }
+
+ it 'revokes human-owned tokens created over 1 year ago' do
+ subject.run!(**args)
+
+ expect(PersonalAccessToken.active).to contain_exactly(
+ unused_token,
+ old_unused_project_access_token,
+ old_unused_token_for_non_group_member,
+ old_unused_token_for_subgroup_member
+ )
+ expect(PersonalAccessToken.revoked).to contain_exactly(
+ old_unused_token,
+ old_actively_used_token,
+ old_formerly_used_token
+ )
+ end
+ end
+
+ it 'updates updated_at' do
+ expect do
+ subject.run!(**args)
+ end.to change {
+ old_unused_token.reload.updated_at
+ }
+ end
+
+ it 'logs action as done' do
+ message = {
+ dry_run: false,
+ token_count: 2,
+ updated_count: 2,
+ tokens: instance_of(Array),
+ group_full_path: group_full_path
+ }
+ expect(logger).to receive(:info).with(include(message))
+ subject.run!(**args)
+ end
+ end
+
+ context 'in a dry run' do
+ # Dry run is the default
+ let(:args) { {} }
+
+ it 'does not revoke any tokens' do
+ expect do
+ subject.run!(**args)
+ end.to not_change {
+ PersonalAccessToken.active.count
+ }
+ end
+
+ it 'logs what could be revoked' do
+ message = {
+ dry_run: true,
+ token_count: 2,
+ updated_count: 0,
+ tokens: instance_of(Array),
+ group_full_path: group_full_path
+ }
+ expect(logger).to receive(:info).with(include(message))
+ subject.run!(**args)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 279486aa2a1..1422f83c629 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::ClosingIssueExtractor do
let_it_be_with_reload(:project) { create(:project) }
- let_it_be(:project2) { create(:project) }
+ let_it_be_with_reload(:project2) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:issue2) { create(:issue, project: project2) }
@@ -335,6 +335,17 @@ RSpec.describe Gitlab::ClosingIssueExtractor do
end
end
+ context 'when target project has autoclose issues disabled' do
+ before do
+ project2.update!(autoclose_referenced_issues: false)
+ end
+
+ it 'omits the issue reference' do
+ message = "Closes #{cross_reference}"
+ expect(subject.closed_by_message(message)).to be_empty
+ end
+ end
+
context "with an invalid URL" do
it do
message = "Closes https://google.com#{urls.project_issue_path(issue2.project, issue2)}"
@@ -443,14 +454,19 @@ RSpec.describe Gitlab::ClosingIssueExtractor do
end
context "with autoclose referenced issues disabled" do
- before do
+ before_all do
project.update!(autoclose_referenced_issues: false)
end
- it do
+ it 'excludes same project references' do
message = "Awesome commit (Closes #{reference})"
expect(subject.closed_by_message(message)).to eq([])
end
+
+ it 'includes issues from other projects with autoclose enabled' do
+ message = "Closes #{cross_reference}"
+ expect(subject.closed_by_message(message)).to eq([issue2])
+ end
end
end
diff --git a/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb b/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb
index 948de161235..cf532cf7be6 100644
--- a/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb
+++ b/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Cluster::PumaWorkerKillerObserver do
let(:counter) { Gitlab::Metrics::NullMetric.instance }
diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb
index 1e7880ed898..8a207bddaae 100644
--- a/spec/lib/gitlab/config/entry/attributable_spec.rb
+++ b/spec/lib/gitlab/config/entry/attributable_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Config::Entry::Attributable do
let(:node) do
diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
index f64b39231a3..331c9efc741 100644
--- a/spec/lib/gitlab/config/entry/composable_hash_spec.rb
+++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do
let(:valid_config) do
{
DATABASE_SECRET: 'passw0rd',
- API_TOKEN: 'passw0rd2'
+ API_TOKEN: 'passw0rd2',
+ ACCEPT_PASSWORD: false
}
end
@@ -55,6 +56,12 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do
expect(entry[:API_TOKEN].metadata).to eq(name: :API_TOKEN)
expect(entry[:API_TOKEN].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
expect(entry[:API_TOKEN].value).to eq('passw0rd2')
+ expect(entry[:ACCEPT_PASSWORD]).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry[:ACCEPT_PASSWORD].description).to eq('ACCEPT_PASSWORD node definition')
+ expect(entry[:ACCEPT_PASSWORD].key).to eq(:ACCEPT_PASSWORD)
+ expect(entry[:ACCEPT_PASSWORD].metadata).to eq(name: :ACCEPT_PASSWORD)
+ expect(entry[:ACCEPT_PASSWORD].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
+ expect(entry[:ACCEPT_PASSWORD].value).to eq(false)
end
describe '#descendants' do
diff --git a/spec/lib/gitlab/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
index f9088130037..fbbc9571eb0 100644
--- a/spec/lib/gitlab/config/entry/simplifiable_spec.rb
+++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Config::Entry::Simplifiable do
describe '.strategy' do
diff --git a/spec/lib/gitlab/config/entry/undefined_spec.rb b/spec/lib/gitlab/config/entry/undefined_spec.rb
index 31e0f9487aa..faa9b9b8a7c 100644
--- a/spec/lib/gitlab/config/entry/undefined_spec.rb
+++ b/spec/lib/gitlab/config/entry/undefined_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Config::Entry::Undefined do
let(:entry) { described_class.new }
diff --git a/spec/lib/gitlab/config/entry/unspecified_spec.rb b/spec/lib/gitlab/config/entry/unspecified_spec.rb
index 35ba992f62a..8fc0889367f 100644
--- a/spec/lib/gitlab/config/entry/unspecified_spec.rb
+++ b/spec/lib/gitlab/config/entry/unspecified_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Config::Entry::Unspecified do
let(:unspecified) { described_class.new(entry) }
diff --git a/spec/lib/gitlab/container_repository/tags/cache_spec.rb b/spec/lib/gitlab/container_repository/tags/cache_spec.rb
index f84c1ce173f..fcfc8e7a348 100644
--- a/spec/lib/gitlab/container_repository/tags/cache_spec.rb
+++ b/spec/lib/gitlab/container_repository/tags/cache_spec.rb
@@ -79,10 +79,14 @@ RSpec.describe ::Gitlab::ContainerRepository::Tags::Cache, :clean_gitlab_redis_c
it 'inserts values in redis' do
::Gitlab::Redis::Cache.with do |redis|
- expect(redis)
- .to receive(:set)
- .with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i)
- .and_call_original
+ expect(redis).to receive(:pipelined).and_call_original
+
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline)
+ .to receive(:set)
+ .with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i)
+ .and_call_original
+ end
end
subject
diff --git a/spec/lib/gitlab/cross_project_access/check_collection_spec.rb b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb
index 178188f5555..a75c943aaf6 100644
--- a/spec/lib/gitlab/cross_project_access/check_collection_spec.rb
+++ b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::CrossProjectAccess::CheckCollection do
subject(:collection) { described_class.new }
diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb
index 5327030daf0..7cf2309a1f8 100644
--- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb
+++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::CrossProjectAccess::CheckInfo do
let(:dummy_controller) { double }
diff --git a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb
index afc45c86362..3a6e528c9b0 100644
--- a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb
+++ b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::CrossProjectAccess::ClassMethods do
let(:dummy_class) do
diff --git a/spec/lib/gitlab/cross_project_access_spec.rb b/spec/lib/gitlab/cross_project_access_spec.rb
index fb72b85f161..e45c734a003 100644
--- a/spec/lib/gitlab/cross_project_access_spec.rb
+++ b/spec/lib/gitlab/cross_project_access_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::CrossProjectAccess do
let(:super_class) { Class.new }
diff --git a/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
index c955b288500..41b0604bee0 100644
--- a/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::CycleAnalytics::Summary::Value do
describe Gitlab::CycleAnalytics::Summary::Value::None do
diff --git a/spec/lib/gitlab/data_builder/issuable_spec.rb b/spec/lib/gitlab/data_builder/issuable_spec.rb
index f0802f335f4..455800a3f7d 100644
--- a/spec/lib/gitlab/data_builder/issuable_spec.rb
+++ b/spec/lib/gitlab/data_builder/issuable_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::DataBuilder::Issuable do
},
assignees: {
previous: [],
- current: [{
+ current: [{
name: "Foo Bar",
username: "foobar",
avatar_url: "http://www.example.com/my-avatar.jpg"
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 3fa535dd800..8e8b8ce6681 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -49,8 +49,7 @@ RSpec.describe Gitlab::DataBuilder::Note do
let(:label) { create(:label, project: project) }
let(:issue) do
- create(:labeled_issue, created_at: fixed_time, updated_at: fixed_time,
- project: project, labels: [label])
+ create(:labeled_issue, created_at: fixed_time, updated_at: fixed_time, project: project, labels: [label])
end
let(:note) do
@@ -84,15 +83,15 @@ RSpec.describe Gitlab::DataBuilder::Note do
describe 'When asking for a note on merge request' do
let(:label) { create(:label, project: project) }
let(:merge_request) do
- create(:labeled_merge_request, created_at: fixed_time,
- updated_at: fixed_time,
- source_project: project,
- labels: [label])
+ create(:labeled_merge_request,
+ created_at: fixed_time,
+ updated_at: fixed_time,
+ source_project: project,
+ labels: [label])
end
let(:note) do
- create(:note_on_merge_request, noteable: merge_request,
- project: project)
+ create(:note_on_merge_request, noteable: merge_request, project: project)
end
it_behaves_like 'includes general data'
@@ -112,14 +111,15 @@ RSpec.describe Gitlab::DataBuilder::Note do
describe 'When asking for a note on merge request diff' do
let(:label) { create(:label, project: project) }
let(:merge_request) do
- create(:labeled_merge_request, created_at: fixed_time, updated_at: fixed_time,
- source_project: project,
- labels: [label])
+ create(:labeled_merge_request,
+ created_at: fixed_time,
+ updated_at: fixed_time,
+ source_project:
+ project, labels: [label])
end
let(:note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- project: project)
+ create(:diff_note_on_merge_request, noteable: merge_request, project: project)
end
it_behaves_like 'includes general data'
@@ -138,13 +138,11 @@ RSpec.describe Gitlab::DataBuilder::Note do
describe 'When asking for a note on project snippet' do
let!(:snippet) do
- create(:project_snippet, created_at: fixed_time, updated_at: fixed_time,
- project: project)
+ create(:project_snippet, created_at: fixed_time, updated_at: fixed_time, project: project)
end
let!(:note) do
- create(:note_on_project_snippet, noteable: snippet,
- project: project)
+ create(:note_on_project_snippet, noteable: snippet, project: project)
end
it_behaves_like 'includes general data'
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index 06c2bc32db3..3daed2508a2 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -59,6 +59,50 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
+ describe '#pause!' do
+ context 'when an invalid transition is applied' do
+ %i[finished failed finalizing].each do |state|
+ it 'raises an exception' do
+ batched_migration = create(:batched_background_migration, state)
+
+ expect { batched_migration.pause! }.to raise_error(StateMachines::InvalidTransition, /Cannot transition status/)
+ end
+ end
+ end
+
+ context 'when a valid transition is applied' do
+ %i[active paused].each do |state|
+ it 'moves to pause' do
+ batched_migration = create(:batched_background_migration, state)
+
+ expect(batched_migration.pause!).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe '#execute!' do
+ context 'when an invalid transition is applied' do
+ %i[finished finalizing].each do |state|
+ it 'raises an exception' do
+ batched_migration = create(:batched_background_migration, state)
+
+ expect { batched_migration.execute! }.to raise_error(StateMachines::InvalidTransition, /Cannot transition status/)
+ end
+ end
+ end
+
+ context 'when a valid transition is applied' do
+ %i[active paused failed].each do |state|
+ it 'moves to active' do
+ batched_migration = create(:batched_background_migration, state)
+
+ expect(batched_migration.execute!).to be_truthy
+ end
+ end
+ end
+ end
+
describe '.valid_status' do
valid_status = [:paused, :active, :finished, :failed, :finalizing]
@@ -77,6 +121,16 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
+ describe '.ordered_by_created_at_desc' do
+ let!(:migration_1) { create(:batched_background_migration, created_at: Time.zone.now - 2) }
+ let!(:migration_2) { create(:batched_background_migration, created_at: Time.zone.now - 1) }
+ let!(:migration_3) { create(:batched_background_migration, created_at: Time.zone.now - 3) }
+
+ it 'returns batched migrations ordered by created_at (DESC)' do
+ expect(described_class.ordered_by_created_at_desc).to eq([migration_2, migration_1, migration_3])
+ end
+ end
+
describe '.active_migration' do
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
let!(:migration1) { create(:batched_background_migration, :finished) }
@@ -620,7 +674,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#progress' do
subject { migration.progress }
- context 'when the migration is finished' do
+ context 'when the migration is completed' do
let(:migration) do
create(:batched_background_migration, :finished, total_tuple_count: 1).tap do |record|
create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 1)
@@ -632,6 +686,18 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
+ context 'when the status is finished' do
+ let(:migration) do
+ create(:batched_background_migration, :finished, total_tuple_count: 100).tap do |record|
+ create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 5)
+ end
+ end
+
+ it 'returns 100' do
+ expect(subject).to be 100
+ end
+ end
+
context 'when the migration does not have jobs' do
let(:migration) { create(:batched_background_migration, :active) }
diff --git a/spec/lib/gitlab/database/batch_average_counter_spec.rb b/spec/lib/gitlab/database/batch_average_counter_spec.rb
new file mode 100644
index 00000000000..43c7a1554f7
--- /dev/null
+++ b/spec/lib/gitlab/database/batch_average_counter_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BatchAverageCounter do
+ let(:model) { Issue }
+ let(:column) { :weight }
+
+ let(:in_transaction) { false }
+
+ before do
+ allow(model.connection).to receive(:transaction_open?).and_return(in_transaction)
+ end
+
+ describe '#count' do
+ before do
+ create_list(:issue, 2, weight: 4)
+ create_list(:issue, 2, weight: 2)
+ create_list(:issue, 2, weight: 3)
+ end
+
+ subject(:batch_average_counter) { described_class.new(model, column) }
+
+ it 'returns correct average of weights' do
+ expect(subject.count).to eq(3.0)
+ end
+
+ it 'does no raise an exception if transaction is not open' do
+ expect { subject.count }.not_to raise_error
+ end
+
+ context 'when transaction is open' do
+ let(:in_transaction) { true }
+
+ it 'raises an error' do
+ expect { subject.count }.to raise_error('BatchAverageCounter can not be run inside a transaction')
+ end
+ end
+
+ context 'when batch size is small' do
+ let(:batch_size) { 2 }
+
+ it 'returns correct average of weights' do
+ expect(subject.count(batch_size: batch_size)).to eq(3.0)
+ end
+ end
+
+ context 'when column passed is an Arel attribute' do
+ let(:column) { model.arel_table[:weight] }
+
+ it 'returns correct average of weights' do
+ expect(subject.count).to eq(3.0)
+ end
+ end
+
+ context 'when column has total count of zero' do
+ before do
+ Issue.update_all(weight: nil)
+ end
+
+ it 'returns the fallback value' do
+ expect(subject.count).to eq(-1)
+ end
+ end
+
+ context 'when one batch has nil weights (no average)' do
+ before do
+ issues = Issue.where(weight: 4)
+ issues.update_all(weight: nil)
+ end
+
+ let(:batch_size) { 2 }
+
+ it 'calculates average of weights with no errors' do
+ expect(subject.count(batch_size: batch_size)).to eq(2.5)
+ end
+ end
+
+ context 'when batch fetch query is cancelled' do
+ let(:batch_size) { 22_000 }
+ let(:relation) { instance_double(ActiveRecord::Relation, to_sql: batch_average_query) }
+
+ context 'when all retries fail' do
+ let(:batch_average_query) { 'SELECT AVG(weight) FROM issues WHERE weight BETWEEN 2 and 5' }
+ let(:query_timed_out_exception) { ActiveRecord::QueryCanceled.new('query timed out') }
+
+ before do
+ allow(model).to receive(:where).and_return(relation)
+ allow(relation).to receive(:pick).and_raise(query_timed_out_exception)
+ end
+
+ it 'logs failing query' do
+ expect(Gitlab::AppJsonLogger).to receive(:error).with(
+ event: 'batch_count',
+ relation: model.table_name,
+ operation: 'average',
+ start: 2,
+ query: batch_average_query,
+ message: 'Query has been canceled with message: query timed out'
+ )
+
+ expect(subject.count(batch_size: batch_size)).to eq(-1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index 811d4fad95c..a87b0c1a3a8 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -86,48 +86,48 @@ RSpec.describe Gitlab::Database::BatchCount do
query: batch_count_query,
message: 'Query has been canceled with message: query timed out'
)
- expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(-1)
+ expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(fallback)
end
end
end
describe '#batch_count' do
it 'counts table' do
- expect(described_class.batch_count(model)).to eq(5)
+ expect(described_class.batch_count(model)).to eq(model.count)
end
it 'counts with :id field' do
- expect(described_class.batch_count(model, :id)).to eq(5)
+ expect(described_class.batch_count(model, :id)).to eq(model.count)
end
it 'counts with "id" field' do
- expect(described_class.batch_count(model, 'id')).to eq(5)
+ expect(described_class.batch_count(model, 'id')).to eq(model.count)
end
it 'counts with table.id field' do
- expect(described_class.batch_count(model, "#{model.table_name}.id")).to eq(5)
+ expect(described_class.batch_count(model, "#{model.table_name}.id")).to eq(model.count)
end
it 'counts with Arel column' do
- expect(described_class.batch_count(model, model.arel_table[:id])).to eq(5)
+ expect(described_class.batch_count(model, model.arel_table[:id])).to eq(model.count)
end
it 'counts table with batch_size 50K' do
- expect(described_class.batch_count(model, batch_size: 50_000)).to eq(5)
+ expect(described_class.batch_count(model, batch_size: 50_000)).to eq(model.count)
end
it 'will not count table with a batch size less than allowed' do
expect(described_class.batch_count(model, batch_size: small_batch_size)).to eq(fallback)
end
- it 'counts with a small edge case batch_sizes than result' do
+ it 'produces the same result with different batch sizes' do
stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
- [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_count(model, batch_size: i)).to eq(5) }
+ [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_count(model, batch_size: i)).to eq(model.count) }
end
it 'counts with a start and finish' do
- expect(described_class.batch_count(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq(5)
+ expect(described_class.batch_count(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq(model.count)
end
it 'stops counting when finish value is reached' do
@@ -217,6 +217,113 @@ RSpec.describe Gitlab::Database::BatchCount do
end
end
+ describe '#batch_count_with_timeout' do
+ it 'counts table' do
+ expect(described_class.batch_count_with_timeout(model)).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'counts with :id field' do
+ expect(described_class.batch_count_with_timeout(model, :id)).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'counts with "id" field' do
+ expect(described_class.batch_count_with_timeout(model, 'id')).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'counts with table.id field' do
+ expect(described_class.batch_count_with_timeout(model, "#{model.table_name}.id")).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'counts with Arel column' do
+ expect(described_class.batch_count_with_timeout(model, model.arel_table[:id])).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'counts table with batch_size 50K' do
+ expect(described_class.batch_count_with_timeout(model, batch_size: 50_000)).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'will not count table with a batch size less than allowed' do
+ expect(described_class.batch_count_with_timeout(model, batch_size: small_batch_size)).to eq({ status: :bad_config })
+ end
+
+ it 'produces the same result with different batch sizes' do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
+
+ [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_count_with_timeout(model, batch_size: i)).to eq({ status: :completed, count: model.count }) }
+ end
+
+ it 'counts with a start and finish' do
+ expect(described_class.batch_count_with_timeout(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq({ status: :completed, count: model.count })
+ end
+
+ it 'stops counting when finish value is reached' do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
+
+ expect(described_class.batch_count_with_timeout(model,
+ start: model.minimum(:id),
+ finish: model.maximum(:id) - 1, # Do not count the last record
+ batch_size: model.count - 2 # Ensure there are multiple batches
+ )).to eq({ status: :completed, count: model.count - 1 })
+ end
+
+ it 'returns a partial count when timeout elapses' do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
+
+ expect(::Gitlab::Metrics::System).to receive(:monotonic_time).and_return(1, 10, 300)
+
+ expect(
+ described_class.batch_count_with_timeout(model, batch_size: 1, timeout: 250.seconds)
+ ).to eq({ status: :timeout, partial_results: 1, continue_from: model.minimum(:id) + 1 })
+ end
+
+ it 'starts counting from a given partial result' do
+ expect(described_class.batch_count_with_timeout(model, partial_results: 3)).to eq({ status: :completed, count: 3 + model.count })
+ end
+
+ it_behaves_like 'when a transaction is open' do
+ subject { described_class.batch_count_with_timeout(model) }
+ end
+
+ it_behaves_like 'when batch fetch query is canceled' do
+ let(:mode) { :itself }
+ let(:operation) { :count }
+ let(:operation_args) { nil }
+ let(:column) { nil }
+ let(:fallback) { { status: :cancelled } }
+
+ subject { described_class.method(:batch_count_with_timeout) }
+ end
+
+ context 'disallowed_configurations' do
+ include_examples 'disallowed configurations', :batch_count do
+ let(:args) { [Issue] }
+ let(:default_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE }
+ end
+
+ it 'raises an error if distinct count is requested' do
+ expect { described_class.batch_count_with_timeout(model.distinct(column)) }.to raise_error 'Use distinct count for optimized distinct counting'
+ end
+ end
+
+ context 'when a relation is grouped' do
+ let!(:one_more_issue) { create(:issue, author: user, project: model.first.project) }
+
+ before do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 1)
+ end
+
+ context 'count by default column' do
+ let(:count) do
+ described_class.batch_count_with_timeout(model.group(column), batch_size: 2)
+ end
+
+ it 'counts grouped records' do
+ expect(count).to eq({ status: :completed, count: { user.id => 4, another_user.id => 2 } })
+ end
+ end
+ end
+ end
+
describe '#batch_distinct_count' do
it 'counts with column field' do
expect(described_class.batch_distinct_count(model, column)).to eq(2)
@@ -242,7 +349,7 @@ RSpec.describe Gitlab::Database::BatchCount do
expect(described_class.batch_distinct_count(model, column, batch_size: small_batch_size)).to eq(fallback)
end
- it 'counts with a small edge case batch_sizes than result' do
+ it 'produces the same result with different batch sizes' do
stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
[1, 2, 4, 5, 6].each { |i| expect(described_class.batch_distinct_count(model, column, batch_size: i)).to eq(2) }
@@ -386,56 +493,18 @@ RSpec.describe Gitlab::Database::BatchCount do
end
describe '#batch_average' do
- let(:model) { Issue }
let(:column) { :weight }
before do
- Issue.update_all(weight: 2)
- end
-
- it 'returns the average of values in the given column' do
- expect(described_class.batch_average(model, column)).to eq(2)
- end
-
- it 'works when given an Arel column' do
- expect(described_class.batch_average(model, model.arel_table[column])).to eq(2)
- end
-
- it 'works with a batch size of 50K' do
- expect(described_class.batch_average(model, column, batch_size: 50_000)).to eq(2)
- end
-
- it 'works with start and finish provided' do
- expect(described_class.batch_average(model, column, start: model.minimum(:id), finish: model.maximum(:id))).to eq(2)
+ allow_next_instance_of(Gitlab::Database::BatchAverageCounter) do |instance|
+ allow(instance).to receive(:count).and_return
+ end
end
- it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE}" do
- min_id = model.minimum(:id)
- relation = instance_double(ActiveRecord::Relation)
- allow(model).to receive_message_chain(:select, public_send: relation)
- batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE)
-
- expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
+ it 'calls BatchAverageCounter' do
+ expect(Gitlab::Database::BatchAverageCounter).to receive(:new).with(model, column).and_call_original
described_class.batch_average(model, column)
end
-
- it_behaves_like 'when a transaction is open' do
- subject { described_class.batch_average(model, column) }
- end
-
- it_behaves_like 'disallowed configurations', :batch_average do
- let(:args) { [model, column] }
- let(:default_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE }
- let(:small_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_AVERAGE_BATCH_SIZE - 1 }
- end
-
- it_behaves_like 'when batch fetch query is canceled' do
- let(:mode) { :itself }
- let(:operation) { :average }
- let(:operation_args) { [column] }
-
- subject { described_class.method(:batch_average) }
- end
end
end
diff --git a/spec/lib/gitlab/database/lock_writes_manager_spec.rb b/spec/lib/gitlab/database/lock_writes_manager_spec.rb
index eb527d492cf..b1cc8add55a 100644
--- a/spec/lib/gitlab/database/lock_writes_manager_spec.rb
+++ b/spec/lib/gitlab/database/lock_writes_manager_spec.rb
@@ -6,13 +6,15 @@ RSpec.describe Gitlab::Database::LockWritesManager do
let(:connection) { ApplicationRecord.connection }
let(:test_table) { '_test_table' }
let(:logger) { instance_double(Logger) }
+ let(:dry_run) { false }
subject(:lock_writes_manager) do
described_class.new(
table_name: test_table,
connection: connection,
database_name: 'main',
- logger: logger
+ logger: logger,
+ dry_run: dry_run
)
end
@@ -27,6 +29,16 @@ RSpec.describe Gitlab::Database::LockWritesManager do
SQL
end
+ describe "#table_locked_for_writes?" do
+ it 'returns false for a table that is not locked for writes' do
+ expect(subject.table_locked_for_writes?(test_table)).to eq(false)
+ end
+
+ it 'returns true for a table that is locked for writes' do
+ expect { subject.lock_writes }.to change { subject.table_locked_for_writes?(test_table) }.from(false).to(true)
+ end
+ end
+
describe '#lock_writes' do
it 'prevents any writes on the table' do
subject.lock_writes
@@ -84,11 +96,57 @@ RSpec.describe Gitlab::Database::LockWritesManager do
subject.lock_writes
end.to raise_error(ActiveRecord::QueryCanceled)
end
+
+ it 'skips the operation if the table is already locked for writes' do
+ subject.lock_writes
+
+ expect(logger).to receive(:info).with("Skipping lock_writes, because #{test_table} is already locked for writes")
+ expect(connection).not_to receive(:execute).with(/CREATE TRIGGER/)
+
+ expect do
+ subject.lock_writes
+ end.not_to change {
+ number_of_triggers_on(connection, test_table)
+ }
+ end
+
+ context 'when running in dry_run mode' do
+ let(:dry_run) { true }
+
+ it 'prints the sql statement to the logger' do
+ expect(logger).to receive(:info).with("Database: 'main', Table: '#{test_table}': Lock Writes")
+ expected_sql_statement = <<~SQL
+ CREATE TRIGGER gitlab_schema_write_trigger_for_#{test_table}
+ BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE
+ ON #{test_table}
+ FOR EACH STATEMENT EXECUTE FUNCTION gitlab_schema_prevent_write();
+ SQL
+ expect(logger).to receive(:info).with(expected_sql_statement)
+
+ subject.lock_writes
+ end
+
+ it 'does not lock the tables for writes' do
+ subject.lock_writes
+
+ expect do
+ connection.execute("delete from #{test_table}")
+ connection.execute("truncate #{test_table}")
+ end.not_to raise_error
+ end
+ end
end
describe '#unlock_writes' do
before do
- subject.lock_writes
+ # Locking the table without the considering the value of dry_run
+ described_class.new(
+ table_name: test_table,
+ connection: connection,
+ database_name: 'main',
+ logger: logger,
+ dry_run: false
+ ).lock_writes
end
it 'allows writing on the table again' do
@@ -114,6 +172,28 @@ RSpec.describe Gitlab::Database::LockWritesManager do
subject.unlock_writes
end
+
+ context 'when running in dry_run mode' do
+ let(:dry_run) { true }
+
+ it 'prints the sql statement to the logger' do
+ expect(logger).to receive(:info).with("Database: 'main', Table: '#{test_table}': Allow Writes")
+ expected_sql_statement = <<~SQL
+ DROP TRIGGER IF EXISTS gitlab_schema_write_trigger_for_#{test_table} ON #{test_table};
+ SQL
+ expect(logger).to receive(:info).with(expected_sql_statement)
+
+ subject.unlock_writes
+ end
+
+ it 'does not unlock the tables for writes' do
+ subject.unlock_writes
+
+ expect do
+ connection.execute("delete from #{test_table}")
+ end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/)
+ end
+ end
end
def number_of_triggers_on(connection, table_name)
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index dd5ad40d8ef..d73b478ee7c 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -667,7 +667,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
column: :user_id,
on_delete: :cascade,
name: name,
- primary_key: :id).and_return(true)
+ primary_key: :id).and_return(true)
expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
index 9451a6bd34a..3ac483c8ab7 100644
--- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
+++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
connection.execute(<<~SQL)
CREATE TABLE #{table_name} (
id bigint primary key not null,
- data bigint
+ data bigint default 0
);
insert into #{table_name} (id) select i from generate_series(1, 1000) g(i);
@@ -40,10 +40,12 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
:id, :data,
batch_size: 100,
job_interval: 5.minutes) # job_interval is skipped when testing
- described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 1.minute)
- unmigrated_row_count = define_batchable_model(table_name).where('id != data').count
- expect(unmigrated_row_count).to eq(0)
+ # Expect that running sampling for this migration processes some of the rows. Sampling doesn't run
+ # over every row in the table, so this does not completely migrate the table.
+ expect { described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 1.minute) }
+ .to change { define_batchable_model(table_name).where('id IS DISTINCT FROM data').count }
+ .by_at_most(-1)
end
end
@@ -62,7 +64,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 3.minutes)
- expect(calls.count).to eq(10) # 1000 rows / batch size 100 = 10
+ expect(calls).not_to be_empty
end
context 'with multiple jobs to run' do
@@ -92,4 +94,19 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
end
end
end
+
+ context 'choosing uniform batches to run' do
+ subject { described_class.new(result_dir: result_dir, connection: connection) }
+
+ describe '#uniform_fractions' do
+ it 'generates evenly distributed sequences of fractions' do
+ received = subject.uniform_fractions.take(9)
+ expected = [0, 1, 1.0 / 2, 1.0 / 4, 3.0 / 4, 1.0 / 8, 3.0 / 8, 5.0 / 8, 7.0 / 8]
+
+ # All the fraction numerators are small integers, and all denominators are powers of 2, so these
+ # fit perfectly into floating point numbers with zero loss of precision
+ expect(received).to eq(expected)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
new file mode 100644
index 00000000000..af7d751a404
--- /dev/null
+++ b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition do
+ include Gitlab::Database::DynamicModelHelpers
+ include Database::TableSchemaHelpers
+
+ let(:migration_context) { Gitlab::Database::Migration[2.0].new }
+
+ let(:connection) { migration_context.connection }
+ let(:table_name) { '_test_table_to_partition' }
+ let(:table_identifier) { "#{connection.current_schema}.#{table_name}" }
+ let(:partitioning_column) { :partition_number }
+ let(:partitioning_default) { 1 }
+ let(:referenced_table_name) { '_test_referenced_table' }
+ let(:other_referenced_table_name) { '_test_other_referenced_table' }
+ let(:parent_table_name) { "#{table_name}_parent" }
+
+ let(:model) { define_batchable_model(table_name, connection: connection) }
+
+ let(:parent_model) { define_batchable_model(parent_table_name, connection: connection) }
+
+ let(:converter) do
+ described_class.new(
+ migration_context: migration_context,
+ table_name: table_name,
+ partitioning_column: partitioning_column,
+ parent_table_name: parent_table_name,
+ zero_partition_value: partitioning_default
+ )
+ end
+
+ before do
+ # Suppress printing migration progress
+ allow(migration_context).to receive(:puts)
+ allow(migration_context.connection).to receive(:transaction_open?).and_return(false)
+
+ connection.execute(<<~SQL)
+ create table #{referenced_table_name} (
+ id bigserial primary key not null
+ )
+ SQL
+
+ connection.execute(<<~SQL)
+ create table #{other_referenced_table_name} (
+ id bigserial primary key not null
+ )
+ SQL
+
+ connection.execute(<<~SQL)
+ insert into #{referenced_table_name} default values;
+ insert into #{other_referenced_table_name} default values;
+ SQL
+
+ connection.execute(<<~SQL)
+ create table #{table_name} (
+ id bigserial not null,
+ #{partitioning_column} bigint not null default #{partitioning_default},
+ referenced_id bigint not null references #{referenced_table_name} (id) on delete cascade,
+ other_referenced_id bigint not null references #{other_referenced_table_name} (id) on delete set null,
+ primary key (id, #{partitioning_column})
+ )
+ SQL
+
+ connection.execute(<<~SQL)
+ insert into #{table_name} (referenced_id, other_referenced_id)
+ select #{referenced_table_name}.id, #{other_referenced_table_name}.id
+ from #{referenced_table_name}, #{other_referenced_table_name};
+ SQL
+ end
+
+ describe "#prepare_for_partitioning" do
+ subject(:prepare) { converter.prepare_for_partitioning }
+
+ it 'adds a check constraint' do
+ expect { prepare }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .count
+ }.from(0).to(1)
+ end
+ end
+
+ describe '#revert_prepare_for_partitioning' do
+ before do
+ converter.prepare_for_partitioning
+ end
+
+ subject(:revert_prepare) { converter.revert_preparation_for_partitioning }
+
+ it 'removes a check constraint' do
+ expect { revert_prepare }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier("#{connection.current_schema}.#{table_name}")
+ .count
+ }.from(1).to(0)
+ end
+ end
+
+ describe "#convert_to_zero_partition" do
+ subject(:partition) { converter.partition }
+
+ before do
+ converter.prepare_for_partitioning
+ end
+
+ context 'when the primary key is incorrect' do
+ before do
+ connection.execute(<<~SQL)
+ alter table #{table_name} drop constraint #{table_name}_pkey;
+ alter table #{table_name} add constraint #{table_name}_pkey PRIMARY KEY (id);
+ SQL
+ end
+
+ it 'throws a reasonable error message' do
+ expect { partition }.to raise_error(described_class::UnableToPartition, /#{partitioning_column}/)
+ end
+ end
+
+ context 'when there is not a supporting check constraint' do
+ before do
+ connection.execute(<<~SQL)
+ alter table #{table_name} drop constraint partitioning_constraint;
+ SQL
+ end
+
+ it 'throws a reasonable error message' do
+ expect { partition }.to raise_error(described_class::UnableToPartition, /constraint /)
+ end
+ end
+
+ it 'migrates the table to a partitioned table' do
+ fks_before = migration_context.foreign_keys(table_name)
+
+ partition
+
+ expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1)
+ expect(migration_context.foreign_keys(parent_table_name).map(&:options)).to match_array(fks_before.map(&:options))
+
+ connection.execute(<<~SQL)
+ insert into #{table_name} (referenced_id, other_referenced_id) select #{referenced_table_name}.id, #{other_referenced_table_name}.id from #{referenced_table_name}, #{other_referenced_table_name};
+ SQL
+
+ # Create a second partition
+ connection.execute(<<~SQL)
+ create table #{table_name}2 partition of #{parent_table_name} FOR VALUES IN (2)
+ SQL
+
+ parent_model.create!(partitioning_column => 2, :referenced_id => 1, :other_referenced_id => 1)
+ expect(parent_model.pluck(:id)).to match_array([1, 2, 3])
+ end
+
+ context 'when an error occurs during the conversion' do
+ def fail_first_time
+ # We can't directly use a boolean here, as we need something that will be passed by-reference to the proc
+ fault_status = { faulted: false }
+ proc do |m, *args, **kwargs|
+ next m.call(*args, **kwargs) if fault_status[:faulted]
+
+ fault_status[:faulted] = true
+ raise 'fault!'
+ end
+ end
+
+ def fail_sql_matching(regex)
+ proc do
+ allow(migration_context.connection).to receive(:execute).and_call_original
+ allow(migration_context.connection).to receive(:execute).with(regex).and_wrap_original(&fail_first_time)
+ end
+ end
+
+ def fail_adding_fk(from_table, to_table)
+ proc do
+ allow(migration_context.connection).to receive(:add_foreign_key).and_call_original
+ expect(migration_context.connection).to receive(:add_foreign_key).with(from_table, to_table, any_args)
+ .and_wrap_original(&fail_first_time)
+ end
+ end
+
+ where(:case_name, :fault) do
+ [
+ ["creating parent table", lazy { fail_sql_matching(/CREATE/i) }],
+ ["adding the first foreign key", lazy { fail_adding_fk(parent_table_name, referenced_table_name) }],
+ ["adding the second foreign key", lazy { fail_adding_fk(parent_table_name, other_referenced_table_name) }],
+ ["attaching table", lazy { fail_sql_matching(/ATTACH/i) }]
+ ]
+ end
+
+ before do
+ # Set up the fault that we'd like to inject
+ fault.call
+ end
+
+ with_them do
+ it 'recovers from a fault', :aggregate_failures do
+ expect { converter.partition }.to raise_error(/fault/)
+ expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(0)
+
+ expect { converter.partition }.not_to raise_error
+ expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe '#revert_conversion_to_zero_partition' do
+ before do
+ converter.prepare_for_partitioning
+ converter.partition
+ end
+
+ subject(:revert_conversion) { converter.revert_partitioning }
+
+ it 'detaches the partition' do
+ expect { revert_conversion }.to change {
+ Gitlab::Database::PostgresPartition
+ .for_parent_table(parent_table_name).count
+ }.from(1).to(0)
+ end
+
+ it 'does not drop the child partition' do
+ expect { revert_conversion }.not_to change { table_oid(table_name) }
+ end
+
+ it 'removes the parent table' do
+ expect { revert_conversion }.to change { table_oid(parent_table_name).present? }.from(true).to(false)
+ end
+
+ it 're-adds the check constraint' do
+ expect { revert_conversion }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .count
+ }.by(1)
+ end
+
+ it 'moves sequences back to the original table' do
+ expect { revert_conversion }.to change { converter.send(:sequences_owned_by, table_name).count }.from(0)
+ .and change { converter.send(:sequences_owned_by, parent_table_name).count }.to(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index dca4548a0a3..8027990a546 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -21,20 +21,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) }
let(:connection) { ActiveRecord::Base.connection }
- let(:table) { "issues" }
+ let(:table) { "my_model_example_table" }
let(:partitioning_strategy) do
double(missing_partitions: partitions, extra_partitions: [], after_adding_partitions: nil)
end
- before do
- allow(connection).to receive(:table_exists?).and_call_original
- allow(connection).to receive(:table_exists?).with(table).and_return(true)
- allow(connection).to receive(:execute).and_call_original
- expect(partitioning_strategy).to receive(:validate_and_fix)
-
- stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
- end
-
let(:partitions) do
[
instance_double(Gitlab::Database::Partitioning::TimePartition, table: 'bar', partition_name: 'foo', to_sql: "SELECT 1"),
@@ -42,19 +33,63 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
]
end
- it 'creates the partition' do
- expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
- expect(connection).to receive(:execute).with(partitions.first.to_sql)
- expect(connection).to receive(:execute).with(partitions.second.to_sql)
+ context 'when the given table is partitioned' do
+ before do
+ create_partitioned_table(connection, table)
- sync_partitions
+ allow(connection).to receive(:table_exists?).and_call_original
+ allow(connection).to receive(:table_exists?).with(table).and_return(true)
+ allow(connection).to receive(:execute).and_call_original
+ expect(partitioning_strategy).to receive(:validate_and_fix)
+
+ stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
+ end
+
+ it 'creates the partition' do
+ expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
+ expect(connection).to receive(:execute).with(partitions.first.to_sql)
+ expect(connection).to receive(:execute).with(partitions.second.to_sql)
+
+ sync_partitions
+ end
+
+ context 'with eplicitly provided connection' do
+ let(:connection) { Ci::ApplicationRecord.connection }
+
+ it 'uses the explicitly provided connection when any' do
+ skip_if_multiple_databases_not_setup
+
+ expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
+ expect(connection).to receive(:execute).with(partitions.first.to_sql)
+ expect(connection).to receive(:execute).with(partitions.second.to_sql)
+
+ described_class.new(model, connection: connection).sync_partitions
+ end
+ end
+
+ context 'when an error occurs during partition management' do
+ it 'does not raise an error' do
+ expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)')
+
+ expect { sync_partitions }.not_to raise_error
+ end
+ end
end
- context 'when an error occurs during partition management' do
- it 'does not raise an error' do
- expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)')
+ context 'when the table is not partitioned' do
+ let(:table) { 'this_does_not_need_to_be_real_table' }
+
+ it 'does not try creating the partitions' do
+ expect(connection).not_to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ {
+ message: 'Skipping synching partitions',
+ table_name: table,
+ connection_name: 'main'
+ }
+ )
- expect { sync_partitions }.not_to raise_error
+ sync_partitions
end
end
end
@@ -74,11 +109,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
before do
- connection.execute(<<~SQL)
- CREATE TABLE my_model_example_table
- (id serial not null, created_at timestamptz not null, primary key (id, created_at))
- PARTITION BY RANGE (created_at);
- SQL
+ create_partitioned_table(connection, 'my_model_example_table')
end
it 'creates partitions' do
@@ -98,6 +129,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
before do
+ create_partitioned_table(connection, table)
+
allow(connection).to receive(:table_exists?).and_call_original
allow(connection).to receive(:table_exists?).with(table).and_return(true)
expect(partitioning_strategy).to receive(:validate_and_fix)
@@ -260,4 +293,12 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
expect { described_class.new(my_model).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0))
end
end
+
+ def create_partitioned_table(connection, table)
+ connection.execute(<<~SQL)
+ CREATE TABLE #{table}
+ (id serial not null, created_at timestamptz not null, primary key (id, created_at))
+ PARTITION BY RANGE (created_at);
+ SQL
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
index 04b9fba5b2f..07c2c6606d8 100644
--- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
end
context 'when some partitions are true for detach_partition_if' do
- let(:detach_partition_if) { ->(p) { p != 5 } }
+ let(:detach_partition_if) { ->(p) { p.value != 5 } }
it 'is the leading set of partitions before that value' do
# should not contain partition 2 since it's the default value for the partition column
@@ -181,9 +181,10 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
Class.new(ApplicationRecord) do
include PartitionedTable
- partitioned_by :partition, strategy: :sliding_list,
- next_partition_if: proc { false },
- detach_partition_if: proc { false }
+ partitioned_by :partition,
+ strategy: :sliding_list,
+ next_partition_if: proc { false },
+ detach_partition_if: proc { false }
end
end.to raise_error(/ignored_columns/)
end
@@ -195,7 +196,8 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
self.ignored_columns = [:partition]
- partitioned_by :partition, strategy: :sliding_list,
+ partitioned_by :partition,
+ strategy: :sliding_list,
next_partition_if: proc { false },
detach_partition_if: proc { false }
end
@@ -221,7 +223,8 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
def self.detach_partition_if_wrapper(...)
detach_partition?(...)
end
- partitioned_by :partition, strategy: :sliding_list,
+ partitioned_by :partition,
+ strategy: :sliding_list,
next_partition_if: method(:next_partition_if_wrapper),
detach_partition_if: method(:detach_partition_if_wrapper)
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
index 3072c413246..1885e84ac4c 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
@@ -97,7 +97,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
end
it 'marks each job record as succeeded after processing' do
- create(:background_migration_job, class_name: "::#{described_class.name}",
+ create(:background_migration_job,
+ class_name: "::#{described_class.name}",
arguments: [source1.id, source3.id, source_table, destination_table, unique_key])
expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original
@@ -108,7 +109,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
end
it 'returns the number of job records marked as succeeded' do
- create(:background_migration_job, class_name: "::#{described_class.name}",
+ create(:background_migration_job,
+ class_name: "::#{described_class.name}",
arguments: [source1.id, source3.id, source_table, destination_table, unique_key])
jobs_updated = backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
index edb8ae36c45..7465f69b87c 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
CREATE TABLE #{table_name} (
id serial NOT NULL,
created_at timestamptz NOT NULL,
+ updated_at timestamptz NOT NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
@@ -204,4 +205,30 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
end
end
end
+
+ describe '#find_duplicate_indexes' do
+ context 'when duplicate and non-duplicate indexes exist' do
+ let(:nonduplicate_column_name) { 'updated_at' }
+ let(:nonduplicate_index_name) { 'updated_at_idx' }
+ let(:duplicate_column_name) { 'created_at' }
+ let(:duplicate_index_name1) { 'created_at_idx' }
+ let(:duplicate_index_name2) { 'index_on_created_at' }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE INDEX #{nonduplicate_index_name} ON #{table_name} (#{nonduplicate_column_name});
+ CREATE INDEX #{duplicate_index_name1} ON #{table_name} (#{duplicate_column_name});
+ CREATE INDEX #{duplicate_index_name2} ON #{table_name} (#{duplicate_column_name});
+ SQL
+ end
+
+ subject do
+ migration.find_duplicate_indexes(table_name)
+ end
+
+ it 'finds the duplicate index' do
+ expect(subject).to match_array([match_array([duplicate_index_name1, duplicate_index_name2])])
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 1026b4370a5..8bb9ad2737a 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -41,6 +41,76 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
allow(migration).to receive(:assert_table_is_allowed)
end
+ context 'list partitioning conversion helpers' do
+ shared_examples_for 'delegates to ConvertTableToFirstListPartition' do
+ it 'throws an error if in a transaction' do
+ allow(migration).to receive(:transaction_open?).and_return(true)
+ expect { migrate }.to raise_error(/cannot be run inside a transaction/)
+ end
+
+ it 'delegates to a method on ConvertTableToFirstListPartition' do
+ expect_next_instance_of(Gitlab::Database::Partitioning::ConvertTableToFirstListPartition,
+ migration_context: migration,
+ table_name: source_table,
+ parent_table_name: partitioned_table,
+ partitioning_column: partition_column,
+ zero_partition_value: min_date) do |converter|
+ expect(converter).to receive(expected_method)
+ end
+
+ migrate
+ end
+ end
+
+ describe '#convert_table_to_first_list_partition' do
+ it_behaves_like 'delegates to ConvertTableToFirstListPartition' do
+ let(:expected_method) { :partition }
+ let(:migrate) do
+ migration.convert_table_to_first_list_partition(table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date)
+ end
+ end
+ end
+
+ describe '#revert_converting_table_to_first_list_partition' do
+ it_behaves_like 'delegates to ConvertTableToFirstListPartition' do
+ let(:expected_method) { :revert_partitioning }
+ let(:migrate) do
+ migration.revert_converting_table_to_first_list_partition(table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date)
+ end
+ end
+ end
+
+ describe '#prepare_constraint_for_list_partitioning' do
+ it_behaves_like 'delegates to ConvertTableToFirstListPartition' do
+ let(:expected_method) { :prepare_for_partitioning }
+ let(:migrate) do
+ migration.prepare_constraint_for_list_partitioning(table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date)
+ end
+ end
+ end
+
+ describe '#revert_preparing_constraint_for_list_partitioning' do
+ it_behaves_like 'delegates to ConvertTableToFirstListPartition' do
+ let(:expected_method) { :revert_preparation_for_partitioning }
+ let(:migrate) do
+ migration.revert_preparing_constraint_for_list_partitioning(table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date)
+ end
+ end
+ end
+ end
+
describe '#partition_table_by_date' do
let(:partition_column) { 'created_at' }
let(:old_primary_key) { 'id' }
diff --git a/spec/lib/gitlab/database/partitioning_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb
index 36c8b0811fe..94cdbfb2328 100644
--- a/spec/lib/gitlab/database/partitioning_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::Database::Partitioning do
end
it 'does not call sync_partitions' do
- expect(described_class).to receive(:sync_partitions).never
+ expect(described_class).not_to receive(:sync_partitions)
described_class.sync_partitions_ignore_db_error
end
@@ -64,6 +64,7 @@ RSpec.describe Gitlab::Database::Partitioning do
end
describe '.sync_partitions' do
+ let(:ci_connection) { Ci::ApplicationRecord.connection }
let(:table_names) { %w[partitioning_test1 partitioning_test2] }
let(:models) do
table_names.map do |table_name|
@@ -94,6 +95,38 @@ RSpec.describe Gitlab::Database::Partitioning do
.and change { find_partitions(table_names.last).size }.from(0)
end
+ context 'with multiple databases' do
+ before do
+ table_names.each do |table_name|
+ ci_connection.execute("DROP TABLE IF EXISTS #{table_name}")
+
+ ci_connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id serial not null,
+ created_at timestamptz not null,
+ PRIMARY KEY (id, created_at))
+ PARTITION BY RANGE (created_at);
+ SQL
+ end
+ end
+
+ after do
+ table_names.each do |table_name|
+ ci_connection.execute("DROP TABLE IF EXISTS #{table_name}")
+ end
+ end
+
+ it 'creates partitions in each database' do
+ skip_if_multiple_databases_not_setup
+
+ expect { described_class.sync_partitions(models) }
+ .to change { find_partitions(table_names.first, conn: connection).size }.from(0)
+ .and change { find_partitions(table_names.last, conn: connection).size }.from(0)
+ .and change { find_partitions(table_names.first, conn: ci_connection).size }.from(0)
+ .and change { find_partitions(table_names.last, conn: ci_connection).size }.from(0)
+ end
+ end
+
context 'when no partitioned models are given' do
it 'manages partitions for each registered model' do
described_class.register_models([models.first])
@@ -111,16 +144,44 @@ RSpec.describe Gitlab::Database::Partitioning do
end
context 'when only a specific database is requested' do
+ let(:ci_model) do
+ Class.new(Ci::ApplicationRecord) do
+ include PartitionedTable
+
+ self.table_name = 'partitioning_test3'
+ partitioned_by :created_at, strategy: :monthly
+ end
+ end
+
before do
- allow(models.first).to receive_message_chain('connection_db_config.name').and_return('main')
- allow(models.last).to receive_message_chain('connection_db_config.name').and_return('ci')
+ (table_names + ['partitioning_test3']).each do |table_name|
+ ci_connection.execute("DROP TABLE IF EXISTS #{table_name}")
+
+ ci_connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id serial not null,
+ created_at timestamptz not null,
+ PRIMARY KEY (id, created_at))
+ PARTITION BY RANGE (created_at);
+ SQL
+ end
+ end
+
+ after do
+ (table_names + ['partitioning_test3']).each do |table_name|
+ ci_connection.execute("DROP TABLE IF EXISTS #{table_name}")
+ end
end
it 'manages partitions for models for the given database', :aggregate_failures do
- expect { described_class.sync_partitions(models, only_on: 'ci') }
- .to change { find_partitions(table_names.last).size }.from(0)
+ skip_if_multiple_databases_not_setup
+
+ expect { described_class.sync_partitions([models.first, ci_model], only_on: 'ci') }
+ .to change { find_partitions(ci_model.table_name, conn: ci_connection).size }.from(0)
- expect(find_partitions(table_names.first).size).to eq(0)
+ expect(find_partitions(models.first.table_name).size).to eq(0)
+ expect(find_partitions(models.first.table_name, conn: ci_connection).size).to eq(0)
+ expect(find_partitions(ci_model.table_name).size).to eq(0)
end
end
end
diff --git a/spec/lib/gitlab/database/postgres_constraint_spec.rb b/spec/lib/gitlab/database/postgres_constraint_spec.rb
new file mode 100644
index 00000000000..75084a69115
--- /dev/null
+++ b/spec/lib/gitlab/database/postgres_constraint_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do
+ # PostgresConstraint does not `behaves_like 'a postgres model'` because it does not correspond 1-1 with a single entry
+ # in pg_class
+ let(:schema) { ActiveRecord::Base.connection.current_schema }
+ let(:table_name) { '_test_table' }
+ let(:table_identifier) { "#{schema}.#{table_name}" }
+ let(:referenced_name) { '_test_referenced' }
+ let(:check_constraint_a_positive) { 'check_constraint_a_positive' }
+ let(:check_constraint_a_gt_b) { 'check_constraint_a_gt_b' }
+ let(:invalid_constraint_a) { 'check_constraint_b_positive_invalid' }
+ let(:unique_constraint_a) { "#{table_name}_a_key" }
+
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ create table #{referenced_name} (
+ id bigserial primary key not null
+ );
+
+ create table #{table_name} (
+ id bigserial not null,
+ referenced_id bigint not null references #{referenced_name}(id),
+ a integer unique,
+ b integer,
+ primary key (id, referenced_id),
+ constraint #{check_constraint_a_positive} check (a > 0),
+ constraint #{check_constraint_a_gt_b} check (a > b)
+ );
+
+ alter table #{table_name} add constraint #{invalid_constraint_a} CHECK (a > 1) NOT VALID;
+ SQL
+ end
+
+ describe '#by_table_identifier' do
+ subject(:constraints_for_table) { described_class.by_table_identifier(table_identifier) }
+
+ it 'includes all constraints on the table' do
+ all_constraints_for_table = described_class.all.to_a.select { |c| c.table_identifier == table_identifier }
+ expect(all_constraints_for_table.map(&:oid)).to match_array(constraints_for_table.pluck(:oid))
+ end
+
+ it 'throws an error if the format is incorrect' do
+ expect { described_class.by_table_identifier('not-an-identifier') }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe '#check_constraints' do
+ subject(:check_constraints) { described_class.check_constraints.by_table_identifier(table_identifier) }
+
+ it 'finds check constraints for the table' do
+ expect(check_constraints.map(&:name)).to contain_exactly(check_constraint_a_positive,
+ check_constraint_a_gt_b,
+ invalid_constraint_a)
+ end
+
+ it 'includes columns for the check constraints', :aggregate_failures do
+ expect(check_constraints.find_by(name: check_constraint_a_positive).column_names).to contain_exactly('a')
+ expect(check_constraints.find_by(name: check_constraint_a_gt_b).column_names).to contain_exactly('a', 'b')
+ end
+ end
+
+ describe "#valid" do
+ subject(:valid_constraint_names) { described_class.valid.by_table_identifier(table_identifier).pluck(:name) }
+
+ let(:all_constraint_names) { described_class.by_table_identifier(table_identifier).pluck(:name) }
+
+ it 'excludes invalid constraints' do
+ expect(valid_constraint_names).not_to include(invalid_constraint_a)
+ expect(valid_constraint_names).to match_array(all_constraint_names - [invalid_constraint_a])
+ end
+ end
+
+ describe '#primary_key_constraints' do
+ subject(:pk_constraints) { described_class.primary_key_constraints.by_table_identifier(table_identifier) }
+
+ it 'finds the primary key constraint for the table' do
+ expect(pk_constraints.count).to eq(1)
+ expect(pk_constraints.first.constraint_type).to eq('p')
+ end
+
+ it 'finds the columns in the primary key constraint' do
+ constraint = pk_constraints.first
+ expect(constraint.column_names).to contain_exactly('id', 'referenced_id')
+ end
+ end
+
+ describe '#unique_constraints' do
+ subject(:unique_constraints) { described_class.unique_constraints.by_table_identifier(table_identifier) }
+
+ it 'finds the unique constraints for the table' do
+ expect(unique_constraints.pluck(:name)).to contain_exactly(unique_constraint_a)
+ end
+ end
+
+ describe '#primary_or_unique_constraints' do
+ subject(:pk_or_unique_constraints) do
+ described_class.primary_or_unique_constraints.by_table_identifier(table_identifier)
+ end
+
+ it 'finds primary and unique constraints' do
+ expect(pk_or_unique_constraints.pluck(:name)).to contain_exactly("#{table_name}_pkey", unique_constraint_a)
+ end
+ end
+
+ describe '#including_column' do
+ it 'only matches constraints on the given column' do
+ constraints_on_a = described_class.by_table_identifier(table_identifier).including_column('a').map(&:name)
+ expect(constraints_on_a).to contain_exactly(check_constraint_a_positive, check_constraint_a_gt_b,
+ unique_constraint_a, invalid_constraint_a)
+ end
+ end
+
+ describe '#not_including_column' do
+ it 'only matches constraints not including the given column' do
+ constraints_not_on_a = described_class.by_table_identifier(table_identifier).not_including_column('a').map(&:name)
+
+ expect(constraints_not_on_a).to contain_exactly("#{table_name}_pkey", "#{table_name}_referenced_id_fkey")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb
new file mode 100644
index 00000000000..ef7c7965c09
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningAnalyzer, query_analyzers: false do
+ let(:analyzer) { described_class }
+
+ before do
+ allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return([analyzer])
+ end
+
+ context 'when ci_partitioning_analyze_queries is disabled' do
+ before do
+ stub_feature_flags(ci_partitioning_analyze_queries: false)
+ end
+
+ it 'does not analyze the query' do
+ expect(analyzer).not_to receive(:analyze)
+
+ process_sql(Ci::BuildMetadata, "SELECT 1 FROM ci_builds_metadata")
+ end
+ end
+
+ context 'when ci_partitioning_analyze_queries is enabled' do
+ context 'when analyzing targeted tables' do
+ described_class::ENABLED_TABLES.each do |enabled_table|
+ context 'when querying a non routing table' do
+ it 'tracks exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ process_sql(Ci::ApplicationRecord, "SELECT 1 FROM #{enabled_table}")
+ end
+
+ it 'raises RoutingTableNotUsedError' do
+ expect { process_sql(Ci::ApplicationRecord, "SELECT 1 FROM #{enabled_table}") }
+ .to raise_error(described_class::RoutingTableNotUsedError)
+ end
+ end
+ end
+
+ context 'when updating a record' do
+ it 'raises RoutingTableNotUsedError' do
+ expect { process_sql(Ci::BuildMetadata, "UPDATE ci_builds_metadata SET id = 1") }
+ .to raise_error(described_class::RoutingTableNotUsedError)
+ end
+ end
+
+ context 'when inserting a record' do
+ it 'raises RoutingTableNotUsedError' do
+ expect { process_sql(Ci::BuildMetadata, "INSERT INTO ci_builds_metadata (id) VALUES(1)") }
+ .to raise_error(described_class::RoutingTableNotUsedError)
+ end
+ end
+ end
+
+ context 'when analyzing non targeted table' do
+ it 'does not raise error' do
+ expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM projects") }
+ .not_to raise_error
+ end
+ end
+
+ context 'when querying a routing table' do
+ it 'does not raise error' do
+ expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM p_ci_builds_metadata") }
+ .not_to raise_error
+ end
+ end
+ end
+
+ private
+
+ def process_sql(model, sql)
+ Gitlab::Database::QueryAnalyzer.instance.within do
+ # Skip load balancer and retrieve connection assigned to model
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 495e953f993..4c98185e780 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -46,25 +46,11 @@ RSpec.describe Gitlab::Database::Reindexing do
end
end
- context 'when async index destruction is enabled' do
- it 'executes async index destruction prior to any reindexing actions' do
- stub_feature_flags(database_async_index_destruction: true)
+ it 'executes async index destruction prior to any reindexing actions' do
+ expect(Gitlab::Database::AsyncIndexes).to receive(:drop_pending_indexes!).ordered.exactly(databases_count).times
+ expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times
- expect(Gitlab::Database::AsyncIndexes).to receive(:drop_pending_indexes!).ordered.exactly(databases_count).times
- expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times
-
- described_class.invoke
- end
- end
-
- context 'when async index destruction is disabled' do
- it 'does not execute async index destruction' do
- stub_feature_flags(database_async_index_destruction: false)
-
- expect(Gitlab::Database::AsyncIndexes).not_to receive(:drop_pending_indexes!)
-
- described_class.invoke
- end
+ described_class.invoke
end
context 'calls automatic reindexing' do
diff --git a/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb
new file mode 100644
index 00000000000..97abd6d23bd
--- /dev/null
+++ b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do
+ let(:connection) { ApplicationRecord.connection }
+ let(:tables) { %w[_test_gitlab_main_items _test_gitlab_main_references] }
+
+ subject do
+ described_class.new(connection, tables).execute
+ end
+
+ before do
+ statement = <<~SQL
+ CREATE TABLE _test_gitlab_main_items (id serial NOT NULL PRIMARY KEY);
+
+ CREATE TABLE _test_gitlab_main_references (
+ id serial NOT NULL PRIMARY KEY,
+ item_id BIGINT NOT NULL,
+ CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id)
+ );
+ SQL
+ connection.execute(statement)
+ end
+
+ describe '#execute' do
+ it 'returns the tables sorted by the foreign keys dependency' do
+ expect(subject).to eq([['_test_gitlab_main_references'], ['_test_gitlab_main_items']])
+ end
+
+ it 'returns both tables together if they are strongly connected' do
+ statement = <<~SQL
+ ALTER TABLE _test_gitlab_main_items ADD COLUMN reference_id BIGINT
+ REFERENCES _test_gitlab_main_references(id)
+ SQL
+ connection.execute(statement)
+
+ expect(subject).to eq([tables])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
new file mode 100644
index 00000000000..01af9efd782
--- /dev/null
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -0,0 +1,257 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_base,
+ :suppress_gitlab_schemas_validate_connection do
+ include MigrationsHelpers
+
+ let(:logger) { instance_double(Logger) }
+ let(:dry_run) { false }
+ let(:until_table) { nil }
+ let(:min_batch_size) { 1 }
+ let(:main_connection) { ApplicationRecord.connection }
+ let(:ci_connection) { Ci::ApplicationRecord.connection }
+ let(:test_gitlab_main_table) { '_test_gitlab_main_table' }
+ let(:test_gitlab_ci_table) { '_test_gitlab_ci_table' }
+
+ # Main Database
+ let(:main_db_main_item_model) { table("_test_gitlab_main_items", database: "main") }
+ let(:main_db_main_reference_model) { table("_test_gitlab_main_references", database: "main") }
+ let(:main_db_ci_item_model) { table("_test_gitlab_ci_items", database: "main") }
+ let(:main_db_ci_reference_model) { table("_test_gitlab_ci_references", database: "main") }
+ let(:main_db_shared_item_model) { table("_test_gitlab_shared_items", database: "main") }
+ # CI Database
+ let(:ci_db_main_item_model) { table("_test_gitlab_main_items", database: "ci") }
+ let(:ci_db_main_reference_model) { table("_test_gitlab_main_references", database: "ci") }
+ let(:ci_db_ci_item_model) { table("_test_gitlab_ci_items", database: "ci") }
+ let(:ci_db_ci_reference_model) { table("_test_gitlab_ci_references", database: "ci") }
+ let(:ci_db_shared_item_model) { table("_test_gitlab_shared_items", database: "ci") }
+
+ subject(:truncate_legacy_tables) do
+ described_class.new(
+ database_name: database_name,
+ min_batch_size: min_batch_size,
+ logger: logger,
+ dry_run: dry_run,
+ until_table: until_table
+ ).execute
+ end
+
+ shared_examples 'truncating legacy tables on a database' do
+ before do
+ skip_if_multiple_databases_not_setup
+
+ # Creating some test tables on the main database
+ main_tables_sql = <<~SQL
+ CREATE TABLE _test_gitlab_main_items (id serial NOT NULL PRIMARY KEY);
+
+ CREATE TABLE _test_gitlab_main_references (
+ id serial NOT NULL PRIMARY KEY,
+ item_id BIGINT NOT NULL,
+ CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id)
+ );
+ SQL
+
+ main_connection.execute(main_tables_sql)
+ ci_connection.execute(main_tables_sql)
+
+ ci_tables_sql = <<~SQL
+ CREATE TABLE _test_gitlab_ci_items (id serial NOT NULL PRIMARY KEY);
+
+ CREATE TABLE _test_gitlab_ci_references (
+ id serial NOT NULL PRIMARY KEY,
+ item_id BIGINT NOT NULL,
+ CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_ci_items(id)
+ );
+ SQL
+
+ main_connection.execute(ci_tables_sql)
+ ci_connection.execute(ci_tables_sql)
+
+ internal_tables_sql = <<~SQL
+ CREATE TABLE _test_gitlab_shared_items (id serial NOT NULL PRIMARY KEY);
+ SQL
+
+ main_connection.execute(internal_tables_sql)
+ ci_connection.execute(internal_tables_sql)
+
+ # Filling the tables
+ 5.times do |i|
+ # Main Database
+ main_db_main_item_model.create!(id: i)
+ main_db_main_reference_model.create!(item_id: i)
+ main_db_ci_item_model.create!(id: i)
+ main_db_ci_reference_model.create!(item_id: i)
+ main_db_shared_item_model.create!(id: i)
+ # CI Database
+ ci_db_main_item_model.create!(id: i)
+ ci_db_main_reference_model.create!(item_id: i)
+ ci_db_ci_item_model.create!(id: i)
+ ci_db_ci_reference_model.create!(item_id: i)
+ ci_db_shared_item_model.create!(id: i)
+ end
+
+ allow(Gitlab::Database::GitlabSchema).to receive(:tables_to_schema).and_return(
+ {
+ "_test_gitlab_main_items" => :gitlab_main,
+ "_test_gitlab_main_references" => :gitlab_main,
+ "_test_gitlab_ci_items" => :gitlab_ci,
+ "_test_gitlab_ci_references" => :gitlab_ci,
+ "_test_gitlab_shared_items" => :gitlab_shared,
+ "_test_gitlab_geo_items" => :gitlab_geo
+ }
+ )
+
+ allow(logger).to receive(:info).with(any_args)
+ end
+
+ context 'when the truncated tables are not locked for writes' do
+ it 'raises an error that the tables are not locked for writes' do
+ error_message = /is not locked for writes. Run the rake task gitlab:db:lock_writes first/
+ expect { truncate_legacy_tables }.to raise_error(error_message)
+ end
+ end
+
+ context 'when the truncated tables are locked for writes' do
+ before do
+ legacy_tables_models.map(&:table_name).each do |table|
+ Gitlab::Database::LockWritesManager.new(
+ table_name: table,
+ connection: connection,
+ database_name: database_name
+ ).lock_writes
+ end
+ end
+
+ it 'truncates the legacy tables' do
+ old_counts = legacy_tables_models.map(&:count)
+ expect do
+ truncate_legacy_tables
+ end.to change { legacy_tables_models.map(&:count) }.from(old_counts).to([0] * legacy_tables_models.length)
+ end
+
+ it 'does not affect the other tables' do
+ expect do
+ truncate_legacy_tables
+ end.not_to change { other_tables_models.map(&:count) }
+ end
+
+ it 'logs the sql statements to the logger' do
+ expect(logger).to receive(:info).with("SET LOCAL lock_timeout = 0")
+ expect(logger).to receive(:info).with("SET LOCAL statement_timeout = 0")
+ expect(logger).to receive(:info)
+ .with(/TRUNCATE TABLE #{legacy_tables_models.map(&:table_name).sort.join(', ')} RESTRICT/)
+ truncate_legacy_tables
+ end
+
+ context 'when running in dry_run mode' do
+ let(:dry_run) { true }
+
+ it 'does not truncate the legacy tables if running in dry run mode' do
+ legacy_tables_models = [main_db_ci_reference_model, main_db_ci_reference_model]
+ expect do
+ truncate_legacy_tables
+ end.not_to change { legacy_tables_models.map(&:count) }
+ end
+ end
+
+ context 'when passing until_table parameter' do
+ context 'with a table that exists' do
+ let(:until_table) { referencing_table_model.table_name }
+
+ it 'only truncates until the table specified' do
+ expect do
+ truncate_legacy_tables
+ end.to change(referencing_table_model, :count).by(-5)
+ .and change(referenced_table_model, :count).by(0)
+ end
+ end
+
+ context 'with a table that does not exist' do
+ let(:until_table) { 'foobar' }
+
+ it 'raises an error if the specified table does not exist' do
+ expect do
+ truncate_legacy_tables
+ end.to raise_error(/The table 'foobar' is not within the truncated tables/)
+ end
+ end
+ end
+
+ context 'with geo configured' do
+ let(:geo_connection) { Gitlab::Database.database_base_models[:geo].connection }
+
+ before do
+ skip unless geo_configured?
+ geo_connection.execute('CREATE TABLE _test_gitlab_geo_items (id serial NOT NULL PRIMARY KEY)')
+ geo_connection.execute('INSERT INTO _test_gitlab_geo_items VALUES(generate_series(1, 50))')
+ end
+
+ it 'does not truncate gitlab_geo tables' do
+ expect do
+ truncate_legacy_tables
+ end.not_to change { geo_connection.select_value("select count(*) from _test_gitlab_geo_items") }
+ end
+ end
+ end
+ end
+
+ context 'when truncating gitlab_ci tables on the main database' do
+ let(:connection) { ApplicationRecord.connection }
+ let(:database_name) { "main" }
+ let(:legacy_tables_models) { [main_db_ci_item_model, main_db_ci_reference_model] }
+ let(:referencing_table_model) { main_db_ci_reference_model }
+ let(:referenced_table_model) { main_db_ci_item_model }
+ let(:other_tables_models) do
+ [
+ main_db_main_item_model, main_db_main_reference_model,
+ ci_db_ci_item_model, ci_db_ci_reference_model,
+ ci_db_main_item_model, ci_db_main_reference_model,
+ main_db_shared_item_model, ci_db_shared_item_model
+ ]
+ end
+
+ it_behaves_like 'truncating legacy tables on a database'
+ end
+
+ context 'when truncating gitlab_main tables on the ci database' do
+ let(:connection) { Ci::ApplicationRecord.connection }
+ let(:database_name) { "ci" }
+ let(:legacy_tables_models) { [ci_db_main_item_model, ci_db_main_reference_model] }
+ let(:referencing_table_model) { ci_db_main_reference_model }
+ let(:referenced_table_model) { ci_db_main_item_model }
+ let(:other_tables_models) do
+ [
+ main_db_main_item_model, main_db_main_reference_model,
+ ci_db_ci_item_model, ci_db_ci_reference_model,
+ main_db_ci_item_model, main_db_ci_reference_model,
+ main_db_shared_item_model, ci_db_shared_item_model
+ ]
+ end
+
+ it_behaves_like 'truncating legacy tables on a database'
+ end
+
+ context 'when running in a single database mode' do
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it 'raises an error when truncating the main database that it is a single database setup' do
+ expect do
+ described_class.new(database_name: 'main', min_batch_size: min_batch_size).execute
+ end.to raise_error(/Cannot truncate legacy tables in single-db setup/)
+ end
+
+ it 'raises an error when truncating the ci database that it is a single database setup' do
+ expect do
+ described_class.new(database_name: 'ci', min_batch_size: min_batch_size).execute
+ end.to raise_error(/Cannot truncate legacy tables in single-db setup/)
+ end
+ end
+
+ def geo_configured?
+ !!ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'geo')
+ end
+end
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
index 6601b6658d5..ad91320c6eb 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
@@ -181,8 +181,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService
let(:existing_project) { create(:project, namespace: existing_group) }
before do
- application_setting.update!(instance_administrators_group_id: existing_group.id,
- self_monitoring_project_id: existing_project.id)
+ application_setting.update!(
+ instance_administrators_group_id: existing_group.id, self_monitoring_project_id: existing_project.id)
end
it 'returns success' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 452a662bdcb..c893bca9e62 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -237,6 +237,26 @@ RSpec.describe Gitlab::Database do
end
end
+ it 'does return a valid schema for a replica connection' do
+ with_replica_pool_for(ActiveRecord::Base) do |main_replica_pool|
+ expect(described_class.gitlab_schemas_for_connection(main_replica_pool.connection)).to include(:gitlab_main, :gitlab_shared)
+ end
+
+ with_replica_pool_for(Ci::ApplicationRecord) do |ci_replica_pool|
+ expect(described_class.gitlab_schemas_for_connection(ci_replica_pool.connection)).to include(:gitlab_ci, :gitlab_shared)
+ end
+ end
+
+ def with_replica_pool_for(base_model)
+ config = Gitlab::Database::LoadBalancing::Configuration.new(base_model, [base_model.connection_pool.db_config.host])
+ lb = Gitlab::Database::LoadBalancing::LoadBalancer.new(config)
+ pool = lb.create_replica_connection_pool(1)
+
+ yield pool
+ ensure
+ pool&.disconnect!
+ end
+
context "when there's CI connection", :request_store do
before do
skip_if_multiple_databases_not_setup
diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
index 15f580a3a60..47d09e7a165 100644
--- a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::DependencyLinker::Parser::Gemfile do
describe '#parse' do
diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb
index 2daa8df815d..8feab0f8017 100644
--- a/spec/lib/gitlab/dependency_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::DependencyLinker do
describe '.link' do
diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
index 9ba9271cefc..ca9c156c1ad 100644
--- a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Diff::FileCollectionSorter do
let(:diffs) do
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index 1d1ffc8c275..53e74748234 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -9,33 +9,33 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
{ ".gitignore-false-false-false" =>
[{ line_code: nil, rich_text: nil, text: "@@ -17,3 +17,4 @@ rerun.txt", type: "match", index: 0, old_pos: 17, new_pos: 17 },
{ line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_17_17",
- rich_text: " <span id=\"LC17\" class=\"line\" lang=\"plaintext\">pickle-email-*.html</span>\n",
- text: " pickle-email-*.html",
- type: nil,
- index: 1,
- old_pos: 17,
- new_pos: 17 },
+ rich_text: " <span id=\"LC17\" class=\"line\" lang=\"plaintext\">pickle-email-*.html</span>\n",
+ text: " pickle-email-*.html",
+ type: nil,
+ index: 1,
+ old_pos: 17,
+ new_pos: 17 },
{ line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_18_18",
- rich_text: " <span id=\"LC18\" class=\"line\" lang=\"plaintext\">.project</span>\n",
- text: " .project",
- type: nil,
- index: 2,
- old_pos: 18,
- new_pos: 18 },
+ rich_text: " <span id=\"LC18\" class=\"line\" lang=\"plaintext\">.project</span>\n",
+ text: " .project",
+ type: nil,
+ index: 2,
+ old_pos: 18,
+ new_pos: 18 },
{ line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_19_19",
- rich_text: " <span id=\"LC19\" class=\"line\" lang=\"plaintext\">config/initializers/secret_token.rb</span>\n",
- text: " config/initializers/secret_token.rb",
- type: nil,
- index: 3,
- old_pos: 19,
- new_pos: 19 },
+ rich_text: " <span id=\"LC19\" class=\"line\" lang=\"plaintext\">config/initializers/secret_token.rb</span>\n",
+ text: " config/initializers/secret_token.rb",
+ type: nil,
+ index: 3,
+ old_pos: 19,
+ new_pos: 19 },
{ line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_20_20",
- rich_text: "+<span id=\"LC20\" class=\"line\" lang=\"plaintext\">.DS_Store</span>",
- text: "+.DS_Store",
- type: "new",
- index: 4,
- old_pos: 20,
- new_pos: 20 }] }
+ rich_text: "+<span id=\"LC20\" class=\"line\" lang=\"plaintext\">.DS_Store</span>",
+ text: "+.DS_Store",
+ type: "new",
+ index: 4,
+ old_pos: 20,
+ new_pos: 20 }] }
end
let(:cache_key) { cache.key }
@@ -109,23 +109,59 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
end
shared_examples 'caches missing entries' do
- it 'filters the key/value list of entries to be caches for each invocation' do
- expect(cache).to receive(:write_to_redis_hash)
- .with(hash_including(*paths))
- .once
- .and_call_original
-
- Gitlab::Redis::Cache.with do |redis|
- expect(redis).to receive(:expire).with(cache.key, described_class::EXPIRATION)
+ where(:expiration_period, :renewable_expiration_ff, :short_renewable_expiration_ff) do
+ [
+ [1.day, false, true],
+ [1.day, false, false],
+ [1.hour, true, true],
+ [8.hours, true, false]
+ ]
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(
+ highlight_diffs_renewable_expiration: renewable_expiration_ff,
+ highlight_diffs_short_renewable_expiration: short_renewable_expiration_ff
+ )
end
- 2.times { cache.write_if_empty }
- end
+ it 'filters the key/value list of entries to be caches for each invocation' do
+ expect(cache).to receive(:write_to_redis_hash)
+ .with(hash_including(*paths))
+ .once
+ .and_call_original
- it 'reads from cache once' do
- expect(cache).to receive(:read_cache).once.and_call_original
+ 2.times { cache.write_if_empty }
+ end
- cache.write_if_empty
+ it 'reads from cache once' do
+ expect(cache).to receive(:read_cache).once.and_call_original
+
+ cache.write_if_empty
+ end
+
+ it 'refreshes TTL of the key on read' do
+ cache.write_if_empty
+
+ time_until_expire = 30.minutes
+
+ Gitlab::Redis::Cache.with do |redis|
+ # Emulate that a key is going to expire soon
+ redis.expire(cache.key, time_until_expire)
+
+ expect(redis.ttl(cache.key)).to be <= time_until_expire
+
+ cache.send(:read_cache)
+
+ if renewable_expiration_ff
+ expect(redis.ttl(cache.key)).to be > time_until_expire
+ expect(redis.ttl(cache.key)).to be_within(1.minute).of(expiration_period)
+ else
+ expect(redis.ttl(cache.key)).to be <= time_until_expire
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
index 3670074cc21..87d47e36f6a 100644
--- a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Diff::InlineDiffMarkdownMarker do
describe '#mark' do
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index 6820a7df95e..8ab2a7b64dd 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Diff::InlineDiffMarker do
describe '#mark' do
diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
index f0e710be2e4..98f0c4df204 100644
--- a/spec/lib/gitlab/diff/lines_unfolder_spec.rb
+++ b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
@@ -189,14 +189,14 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do
let(:diff) do
Gitlab::Git::Diff.new({ diff: raw_diff,
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- a_mode: "100644",
- b_mode: "100644",
- new_file: false,
- renamed_file: false,
- deleted_file: false,
- too_large: false })
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ a_mode: "100644",
+ b_mode: "100644",
+ new_file: false,
+ renamed_file: false,
+ deleted_file: false,
+ too_large: false })
end
let(:diff_file) do
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index bb3522eb579..00a468bfef6 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -684,7 +684,7 @@ RSpec.describe Gitlab::Diff::Position do
"old_line" => 18,
"new_line" => 18
},
- "end" => {
+ "end" => {
"line_code" => end_line_code,
"type" => nil,
"old_line" => end_old_line,
diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
index b5137f9db6b..e1135f4d546 100644
--- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
+++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do
include RepoHelpers
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:commit) { project.commit("5d6ed1503801ca9dc28e95eeb85a7cf863527aee") }
let(:diffs) { commit.raw_diffs.to_a }
let(:diff) { diffs.first }
diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb
new file mode 100644
index 00000000000..df17d92bb0c
--- /dev/null
+++ b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DoorkeeperSecretStoring::Secret::Pbkdf2Sha512 do
+ describe '.transform_secret' do
+ let(:plaintext_secret) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' }
+
+ it 'generates a PBKDF2+SHA512 hashed value in the correct format' do
+ expect(described_class.transform_secret(plaintext_secret))
+ .to eq("$pbkdf2-sha512$20000$$.c0G5XJVEew1TyeJk5TrkvB0VyOaTmDzPrsdNRED9vVeZlSyuG3G90F0ow23zUCiWKAVwmNnR/ceh.nJG3MdpQ") # rubocop:disable Layout/LineLength
+ end
+
+ context 'when hash_oauth_secrets is disabled' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
+
+ it 'returns a plaintext secret' do
+ expect(described_class.transform_secret(plaintext_secret)).to eq(plaintext_secret)
+ end
+ end
+ end
+
+ describe 'STRETCHES' do
+ it 'is 20_000' do
+ expect(described_class::STRETCHES).to eq(20_000)
+ end
+ end
+
+ describe 'SALT' do
+ it 'is empty' do
+ expect(described_class::SALT).to be_empty
+ end
+ end
+
+ describe '.secret_matches?' do
+ it "match by hashing the input if the stored value is hashed" do
+ stub_feature_flags(hash_oauth_secrets: false)
+ plain_secret = 'plain_secret'
+ stored_value = '$pbkdf2-sha512$20000$$/BwQRdwSpL16xkQhstavh7nvA5avCP7.4n9LLKe9AupgJDeA7M5xOAvG3N3E5XbRyGWWBbbr.BsojPVWzd1Sqg' # rubocop:disable Layout/LineLength
+ expect(described_class.secret_matches?(plain_secret, stored_value)).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb
index e953733c997..c73744cd481 100644
--- a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb
+++ b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512 do
+RSpec.describe Gitlab::DoorkeeperSecretStoring::Token::Pbkdf2Sha512 do
describe '.transform_secret' do
let(:plaintext_token) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' }
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index b0c67cdafe1..690396d4dbc 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -98,6 +98,36 @@ RSpec.describe Gitlab::EncodingHelper do
end
end
+ describe '#encode_utf8_with_escaping!' do
+ where(:input, :expected) do
+ "abcd" | "abcd"
+ "DzDzDz" | "DzDzDz"
+ "\xC7\xB2\xC7DzDzDz" | "Dz%C7DzDzDz"
+ "🐤🐤🐤🐤\xF0\x9F\x90" | "🐤🐤🐤🐤%F0%9F%90"
+ "\xD0\x9F\xD1\x80 \x90" | "Пр %90"
+ "\x41" | "A"
+ end
+
+ with_them do
+ it 'escapes invalid UTF-8' do
+ expect(ext_class.encode_utf8_with_escaping!(input.dup.force_encoding(Encoding::ASCII_8BIT))).to eq(expected)
+ expect(ext_class.encode_utf8_with_escaping!(input)).to eq(expected)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(escape_gitaly_refs: false)
+ end
+
+ it 'uses #encode! method' do
+ expect(ext_class).to receive(:encode!).with('String')
+
+ ext_class.encode_utf8_with_escaping!('String')
+ end
+ end
+ end
+
describe '#encode_utf8' do
[
["nil", nil, nil],
diff --git a/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb
index 210829056c8..c9b632b50e1 100644
--- a/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb
@@ -38,10 +38,10 @@ RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do
expect(result_hash[:tags])
.to include(priority: 'high',
- locale: 'en',
- program: 'test',
- feature_category: 'feature_a',
- correlation_id: 'cid')
+ locale: 'en',
+ program: 'test',
+ feature_category: 'feature_a',
+ correlation_id: 'cid')
expect(result_hash[:extra])
.to include(some_info: 'info',
diff --git a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
index 577d59798da..3d23249d00d 100644
--- a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
+++ b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::ErrorTracking::StackTraceHighlightDecorator do
'function' => 'print',
'lineNo' => 3,
'filename' => 'hello_world.php',
- 'context' => [
+ 'context' => [
[1, '<span id="LC1" class="line" lang="hack"><span class="c1">// PHP/Hack example</span></span>'],
[2, '<span id="LC1" class="line" lang="hack"><span class="cp">&lt;?php</span></span>'],
[3, '<span id="LC1" class="line" lang="hack"><span class="k">echo</span> <span class="s1">\'Hello, World!\'</span><span class="p">;</span></span>']
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index 8228f95dd5e..da5eaf2e4ab 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -119,11 +119,11 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
let(:expected_items) do
{
etag_route: endpoint,
- params: {},
- format: :html,
- method: 'GET',
- path: enabled_path,
- status: status_code
+ params: {},
+ format: :html,
+ method: 'GET',
+ path: enabled_path,
+ status: status_code
}
end
diff --git a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb
index 9a6787e3640..792f02f8cda 100644
--- a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::EtagCaching::Router::Graphql do
def match_route(path, header)
described_class.match(
double(path_info: path,
- headers: { 'X-GITLAB-GRAPHQL-RESOURCE-ETAG' => header }))
+ headers: { 'X-GITLAB-GRAPHQL-RESOURCE-ETAG' => header }))
end
describe '.cache_key' do
diff --git a/spec/lib/gitlab/experimentation/group_types_spec.rb b/spec/lib/gitlab/experimentation/group_types_spec.rb
index 599ad08f706..2b118d76fa4 100644
--- a/spec/lib/gitlab/experimentation/group_types_spec.rb
+++ b/spec/lib/gitlab/experimentation/group_types_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Experimentation::GroupTypes do
it 'defines a GROUP_CONTROL constant' do
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 8c0c56ea2c3..208acf28cc4 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::FileDetector do
describe '.types_in_paths' do
diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb
index d684beaaaca..54dfde9fc45 100644
--- a/spec/lib/gitlab/file_markdown_link_builder_spec.rb
+++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::FileMarkdownLinkBuilder do
let(:custom_class) do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index 763e6f1b5f4..a16f96a7d11 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Gfm::UploadsRewriter do
end
let(:text) do
- "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}"
+ "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}".freeze # rubocop:disable Style/RedundantFreeze
end
def referenced_files(text, project)
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 0da7aa7dad0..d35d288050a 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -2,11 +2,9 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::Blob, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- let(:rugged) do
- Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH))
- end
+RSpec.describe Gitlab::Git::Blob do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository.raw }
describe 'initialize' do
let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
@@ -44,7 +42,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
shared_examples '.find' do
context 'nil path' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], nil) }
it { expect(blob).to eq(nil) }
end
@@ -56,30 +54,30 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
end
context 'blank path' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], '') }
it { expect(blob).to eq(nil) }
end
context 'file in subdir' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb") }
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
it { expect(blob.path).to eq("files/ruby/popen.rb") }
- it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) }
it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
it { expect(blob.size).to eq(669) }
it { expect(blob.mode).to eq("100644") }
end
context 'file in root' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], ".gitignore") }
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
it { expect(blob.name).to eq(".gitignore") }
it { expect(blob.path).to eq(".gitignore") }
- it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) }
it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
it { expect(blob.size).to eq(241) }
it { expect(blob.mode).to eq("100644") }
@@ -87,25 +85,25 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
end
context 'file in root with leading slash' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "/.gitignore") }
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
it { expect(blob.name).to eq(".gitignore") }
it { expect(blob.path).to eq(".gitignore") }
- it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) }
it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
it { expect(blob.size).to eq(241) }
it { expect(blob.mode).to eq("100644") }
end
context 'non-exist file' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "missing.rb") }
it { expect(blob).to be_nil }
end
context 'six submodule' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], 'six') }
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
it { expect(blob.data).to eq('') }
@@ -121,7 +119,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
end
context 'large file' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg') }
let(:blob_size) { 111803 }
let(:stub_limit) { 1000 }
@@ -159,10 +157,10 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
describe '.find with Rugged enabled', :enable_rugged do
it 'calls out to the Rugged implementation' do
allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
+ allow(instance).to receive(:rev_parse).with(TestEnv::BRANCH_SHA['master']).and_call_original
end
- described_class.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg')
+ described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg')
end
it_behaves_like '.find'
@@ -177,40 +175,13 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
it { expect(raw_blob.size).to eq(669) }
it { expect(raw_blob.truncated?).to be_falsey }
it { expect(bad_blob).to be_nil }
-
- context 'large file' do
- it 'limits the size of a large file' do
- blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1
- buffer = Array.new(blob_size, 0)
- rugged_blob = Rugged::Blob.from_buffer(rugged, buffer.join(''))
- blob = Gitlab::Git::Blob.raw(repository, rugged_blob)
-
- expect(blob.size).to eq(blob_size)
- expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
- expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
- expect(blob.truncated?).to be_truthy
-
- blob.load_all_data!(repository)
- expect(blob.loaded_size).to eq(blob_size)
- end
- end
-
- context 'when sha references a tree' do
- it 'returns nil' do
- tree = rugged.rev_parse('master^{tree}')
-
- blob = Gitlab::Git::Blob.raw(repository, tree.oid)
-
- expect(blob).to be_nil
- end
- end
end
describe '.batch' do
let(:blob_references) do
[
- [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
- [SeedRepo::Commit::ID, 'six']
+ [TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb"],
+ [TestEnv::BRANCH_SHA['master'], 'six']
]
end
@@ -224,7 +195,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
it { expect(blob.path).to eq("files/ruby/popen.rb") }
- it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.commit_id).to eq(TestEnv::BRANCH_SHA['master']) }
it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
it { expect(blob.size).to eq(669) }
it { expect(blob.mode).to eq("100644") }
@@ -273,21 +244,21 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
context 'when large number of blobs requested' do
let(:first_batch) do
[
- [SeedRepo::Commit::ID, 'files/ruby/popen.rb'],
- [SeedRepo::Commit::ID, 'six']
+ [TestEnv::BRANCH_SHA['master'], 'files/ruby/popen.rb'],
+ [TestEnv::BRANCH_SHA['master'], 'six']
]
end
let(:second_batch) do
[
- [SeedRepo::Commit::ID, 'some'],
- [SeedRepo::Commit::ID, 'other']
+ [TestEnv::BRANCH_SHA['master'], 'some'],
+ [TestEnv::BRANCH_SHA['master'], 'other']
]
end
let(:third_batch) do
[
- [SeedRepo::Commit::ID, 'files']
+ [TestEnv::BRANCH_SHA['master'], 'files']
]
end
@@ -315,8 +286,8 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
describe '.batch_metadata' do
let(:blob_references) do
[
- [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
- [SeedRepo::Commit::ID, 'six']
+ [TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb"],
+ [TestEnv::BRANCH_SHA['master'], 'six']
]
end
@@ -333,8 +304,6 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
end
describe '.batch_lfs_pointers' do
- let(:tree_object) { rugged.rev_parse('master^{tree}') }
-
let(:non_lfs_blob) do
Gitlab::Git::Blob.find(
repository,
@@ -346,8 +315,8 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
let(:lfs_blob) do
Gitlab::Git::Blob.find(
repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'files/lfs/image.jpg'
+ TestEnv::BRANCH_SHA['master'],
+ 'files/lfs/lfs_object.iso'
)
end
@@ -374,12 +343,6 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
expect(blobs_2).to eq([])
end
- it 'silently ignores tree objects' do
- blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
-
- expect(blobs).to eq([])
- end
-
it 'silently ignores non lfs objects' do
blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
@@ -398,7 +361,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
describe 'encoding', :aggregate_failures do
context 'file with russian text' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/russian.rb") }
it 'has the correct blob attributes' do
expect(blob.name).to eq("russian.rb")
@@ -412,7 +375,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
end
context 'file with Japanese text' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/テスト.txt") }
it 'has the correct blob attributes' do
expect(blob.name).to eq("テスト.txt")
@@ -424,12 +387,12 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
end
context 'file with ISO-8859 text' do
- let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") }
+ let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/iso8859.txt") }
it 'has the correct blob attributes' do
expect(blob.name).to eq("iso8859.txt")
- expect(blob.loaded_size).to eq(4)
- expect(blob.size).to eq(4)
+ expect(blob.loaded_size).to eq(3)
+ expect(blob.size).to eq(3)
expect(blob.mode).to eq("100644")
expect(blob.truncated?).to be_falsey
end
@@ -441,7 +404,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
- 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ TestEnv::BRANCH_SHA['master'],
'files/ruby/regex.rb'
)
end
@@ -456,14 +419,14 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
- 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ TestEnv::BRANCH_SHA['with-executables'],
'files/executables/ls'
)
end
it { expect(blob.name).to eq('ls') }
it { expect(blob.path).to eq('files/executables/ls') }
- it { expect(blob.size).to eq(110080) }
+ it { expect(blob.size).to eq(23) }
it { expect(blob.mode).to eq("100755") }
end
@@ -471,29 +434,14 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
- 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
- 'files/links/ruby-style-guide.md'
+ '88ce9520c07b7067f589b7f83a30b6250883115c',
+ 'symlink'
)
end
- it { expect(blob.name).to eq('ruby-style-guide.md') }
- it { expect(blob.path).to eq('files/links/ruby-style-guide.md') }
- it { expect(blob.size).to eq(31) }
- it { expect(blob.mode).to eq("120000") }
- end
-
- context 'file symlink to binary' do
- let(:blob) do
- Gitlab::Git::Blob.find(
- repository,
- 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
- 'files/links/touch'
- )
- end
-
- it { expect(blob.name).to eq('touch') }
- it { expect(blob.path).to eq('files/links/touch') }
- it { expect(blob.size).to eq(20) }
+ it { expect(blob.name).to eq('symlink') }
+ it { expect(blob.path).to eq('symlink') }
+ it { expect(blob.size).to eq(6) }
it { expect(blob.mode).to eq("120000") }
end
end
@@ -503,79 +451,20 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'files/lfs/image.jpg'
+ TestEnv::BRANCH_SHA['png-lfs'],
+ 'files/images/emoji.png'
)
end
it { expect(blob.lfs_pointer?).to eq(true) }
- it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
- it { expect(blob.lfs_size).to eq(19548) }
- it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
- it { expect(blob.name).to eq("image.jpg") }
- it { expect(blob.path).to eq("files/lfs/image.jpg") }
- it { expect(blob.size).to eq(130) }
+ it { expect(blob.lfs_oid).to eq("96f74c6fe7a2979eefb9ec74a5dfc6888fb25543cf99b77586b79afea1da6f97") }
+ it { expect(blob.lfs_size).to eq(1219696) }
+ it { expect(blob.id).to eq("ff0ab3afd1616ff78d0331865d922df103b64cf0") }
+ it { expect(blob.name).to eq("emoji.png") }
+ it { expect(blob.path).to eq("files/images/emoji.png") }
+ it { expect(blob.size).to eq(132) }
it { expect(blob.mode).to eq("100644") }
end
-
- describe 'file an invalid lfs pointer' do
- context 'with correct version header but incorrect size and oid' do
- let(:blob) do
- Gitlab::Git::Blob.find(
- repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'files/lfs/archive-invalid.tar'
- )
- end
-
- it { expect(blob.lfs_pointer?).to eq(false) }
- it { expect(blob.lfs_oid).to eq(nil) }
- it { expect(blob.lfs_size).to eq(nil) }
- it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") }
- it { expect(blob.name).to eq("archive-invalid.tar") }
- it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") }
- it { expect(blob.size).to eq(43) }
- it { expect(blob.mode).to eq("100644") }
- end
-
- context 'with correct version header and size but incorrect size and oid' do
- let(:blob) do
- Gitlab::Git::Blob.find(
- repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'files/lfs/picture-invalid.png'
- )
- end
-
- it { expect(blob.lfs_pointer?).to eq(false) }
- it { expect(blob.lfs_oid).to eq(nil) }
- it { expect(blob.lfs_size).to eq(1575078) }
- it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
- it { expect(blob.name).to eq("picture-invalid.png") }
- it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
- it { expect(blob.size).to eq(57) }
- it { expect(blob.mode).to eq("100644") }
- end
-
- context 'with correct version header and size but invalid size and oid' do
- let(:blob) do
- Gitlab::Git::Blob.find(
- repository,
- '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
- 'files/lfs/file-invalid.zip'
- )
- end
-
- it { expect(blob.lfs_pointer?).to eq(false) }
- it { expect(blob.lfs_oid).to eq(nil) }
- it { expect(blob.lfs_size).to eq(nil) }
- it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") }
- it { expect(blob.name).to eq("file-invalid.zip") }
- it { expect(blob.path).to eq("files/lfs/file-invalid.zip") }
- it { expect(blob.size).to eq(60) }
- it { expect(blob.mode).to eq("100644") }
- end
- end
end
describe '#load_all_data!' do
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index feaa1f6595c..95cc833390f 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::Git::Branch do
end
def create_commit
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: 'HEAD',
message: 'commit message',
diff --git a/spec/lib/gitlab/git/changed_path_spec.rb b/spec/lib/gitlab/git/changed_path_spec.rb
index 93db107ad5c..ef51021ba4c 100644
--- a/spec/lib/gitlab/git/changed_path_spec.rb
+++ b/spec/lib/gitlab/git/changed_path_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::ChangedPath do
subject(:changed_path) { described_class.new(path: path, status: status) }
diff --git a/spec/lib/gitlab/git/changes_spec.rb b/spec/lib/gitlab/git/changes_spec.rb
index 310be7a3731..7cded9740ee 100644
--- a/spec/lib/gitlab/git/changes_spec.rb
+++ b/spec/lib/gitlab/git/changes_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::Changes do
let(:changes) { described_class.new }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 95b49186d0f..d873151421d 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -2,8 +2,8 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::Commit, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
+RSpec.describe Gitlab::Git::Commit do
+ let(:repository) { create(:project, :repository).repository.raw }
let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) }
describe "Commit info from gitaly commit" do
@@ -121,14 +121,6 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
it "returns nil for id containing NULL" do
expect(described_class.find(repository, "HE\x00AD")).to be_nil
end
-
- context 'with broken repo' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') }
-
- it 'returns nil' do
- expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil
- end
- end
end
describe '.find with Gitaly enabled' do
@@ -154,7 +146,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
describe '#id' do
subject { super().id }
- it { is_expected.to eq(SeedRepo::LastCommit::ID) }
+ it { is_expected.to eq(TestEnv::BRANCH_SHA['master']) }
end
end
@@ -223,7 +215,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
expect(subject.size).to eq(10)
end
- it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ it { is_expected.to include(TestEnv::BRANCH_SHA['master']) }
end
context 'path is nil' do
@@ -242,28 +234,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
expect(subject.size).to eq(10)
end
- it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
- end
-
- context 'ref is branch name' do
- subject do
- commits = described_class.where(
- repo: repository,
- ref: 'master',
- path: 'files',
- limit: 3,
- offset: 1
- )
-
- commits.map { |c| c.id }
- end
-
- it 'has 3 elements' do
- expect(subject.size).to eq(3)
- end
-
- it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
- it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
+ it { is_expected.to include(TestEnv::BRANCH_SHA['master']) }
end
context 'ref is commit id' do
@@ -323,13 +294,12 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
context 'requesting a commit range' do
let(:from) { 'v1.0.0' }
- let(:to) { 'v1.2.0' }
+ let(:to) { 'v1.1.0' }
let(:commits_in_range) do
%w[
570e7b2abdd848b95f2f578043fc23bd6f6fd24d
5937ac0a7beb003549fc5fd26fc247adbce4a52e
- eb49186cfa5c4338011f5f590fac11bd66c5c631
]
end
@@ -338,9 +308,9 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
end
context 'limited' do
- let(:limit) { 2 }
+ let(:limit) { 1 }
- it { expect(commit_ids).to eq(commits_in_range.last(2)) }
+ it { expect(commit_ids).to eq(commits_in_range.last(1)) }
end
end
end
@@ -383,16 +353,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
commits.map(&:id)
end
- it 'has 34 elements' do
- expect(subject.size).to eq(34)
- end
-
- it 'includes the expected commits' do
- expect(subject).to include(
- SeedRepo::Commit::ID,
- SeedRepo::Commit::PARENT_ID,
- SeedRepo::FirstCommit::ID
- )
+ it 'has maximum elements' do
+ expect(subject.size).to eq(50)
end
end
@@ -408,13 +370,13 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
commits.map(&:id)
end
- it 'has 24 elements' do
- expect(subject.size).to eq(24)
+ it 'has 36 elements' do
+ expect(subject.size).to eq(36)
end
it 'includes the expected commits' do
expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID)
- expect(subject).not_to include(SeedRepo::LastCommit::ID)
+ expect(subject).not_to include(TestEnv::BRANCH_SHA['master'])
end
end
end
@@ -650,8 +612,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
subject { commit.ref_names(repository) }
- it 'has 2 element' do
- expect(subject.size).to eq(2)
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
end
it { is_expected.to include("master") }
@@ -681,6 +643,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
end
it 'gets messages in one batch', :request_store do
+ repository # preload repository so that the project factory does not pollute request counts
+
expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
end
end
diff --git a/spec/lib/gitlab/git/commit_stats_spec.rb b/spec/lib/gitlab/git/commit_stats_spec.rb
index 29d3909efec..81d9dda4b8f 100644
--- a/spec/lib/gitlab/git/commit_stats_spec.rb
+++ b/spec/lib/gitlab/git/commit_stats_spec.rb
@@ -2,17 +2,19 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::CommitStats, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
+RSpec.describe Gitlab::Git::CommitStats do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository.raw }
+
+ let(:commit) { Gitlab::Git::Commit.find(repository, TestEnv::BRANCH_SHA['feature']) }
def verify_stats!
stats = described_class.new(repository, commit)
expect(stats).to have_attributes(
- additions: eq(11),
- deletions: eq(6),
- total: eq(17)
+ additions: eq(5),
+ deletions: eq(0),
+ total: eq(5)
)
end
@@ -21,7 +23,7 @@ RSpec.describe Gitlab::Git::CommitStats, :seed_helper do
verify_stats!
- expect(Rails.cache.fetch("commit_stats:group/project:#{commit.id}")).to eq([11, 6])
+ expect(Rails.cache.fetch("commit_stats:#{repository.gl_project_path}:#{commit.id}")).to eq([5, 0])
expect(repository.gitaly_commit_client).not_to receive(:commit_stats)
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
index 51043355ede..e8c683cf8aa 100644
--- a/spec/lib/gitlab/git/compare_spec.rb
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -2,8 +2,9 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::Compare, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
+RSpec.describe Gitlab::Git::Compare do
+ let_it_be(:repository) { create(:project, :repository).repository.raw }
+
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) }
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) }
diff --git a/spec/lib/gitlab/git/conflict/file_spec.rb b/spec/lib/gitlab/git/conflict/file_spec.rb
index 6eb7a7e394e..fb1bec0a554 100644
--- a/spec/lib/gitlab/git/conflict/file_spec.rb
+++ b/spec/lib/gitlab/git/conflict/file_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::Conflict::File do
let(:conflict) { { ancestor: { path: 'ancestor' }, theirs: { path: 'foo', mode: 33188 }, ours: { path: 'foo', mode: 33188 } } }
diff --git a/spec/lib/gitlab/git/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb
index 7d81af92412..67f288e0299 100644
--- a/spec/lib/gitlab/git/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/git/conflict/parser_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::Conflict::Parser do
describe '.parse' do
diff --git a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb
index 1c49486b7b1..7888e224d59 100644
--- a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb
+++ b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb
@@ -110,7 +110,7 @@ RSpec.describe Gitlab::Git::CrossRepoComparer do
def create_commit(user, repo, branch)
action = { action: :create, file_path: '/FILE', content: 'content' }
- result = repo.multi_action(user, branch_name: branch, message: 'Commit', actions: [action])
+ result = repo.commit_files(user, branch_name: branch, message: 'Commit', actions: [action])
result.newrev
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 0e3e92e03cf..7fa5bd8a92b 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
+RSpec.describe Gitlab::Git::DiffCollection do
before do
stub_const('MutatingConstantIterator', Class.new)
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 2c931a999f1..6745c700b92 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -2,8 +2,10 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::Diff, :seed_helper do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
+RSpec.describe Gitlab::Git::Diff do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository }
+
let(:gitaly_diff) do
Gitlab::GitalyClient::Diff.new(
from_path: '.gitmodules',
@@ -190,16 +192,6 @@ EOT
expect(binary_diff(project).diff).not_to be_empty
end
end
-
- context 'when convert_diff_to_utf8_with_replacement_symbol feature flag is disabled' do
- before do
- stub_feature_flags(convert_diff_to_utf8_with_replacement_symbol: false)
- end
-
- it 'will not try to convert invalid characters' do
- expect(Gitlab::EncodingHelper).not_to receive(:encode_utf8_with_replacement_character?)
- end
- end
end
context 'when replace_invalid_utf8_chars is false' do
@@ -218,7 +210,7 @@ EOT
let(:diffs) { described_class.between(repository, 'feature', 'master', options) }
it 'has the correct size' do
- expect(diffs.size).to eq(24)
+ expect(diffs.size).to eq(21)
end
context 'diff' do
diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
index 0e386c7f3d1..33268b4edcb 100644
--- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb
+++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::GitmodulesParser do
it 'parses a .gitmodules file correctly' do
diff --git a/spec/lib/gitlab/git/lfs_pointer_file_spec.rb b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb
index f45c7cccca0..b210c86c3d1 100644
--- a/spec/lib/gitlab/git/lfs_pointer_file_spec.rb
+++ b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::LfsPointerFile do
let(:data) { "1234\n" }
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index a1fb8b70bd7..9a87911b6e8 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::Repository, :seed_helper do
+RSpec.describe Gitlab::Git::Repository do
include Gitlab::EncodingHelper
include RepoHelpers
using RSpec::Parameterized::TableSyntax
@@ -21,13 +21,11 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
- let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') }
- let(:mutable_repository_path) { File.join(TestEnv.repos_path, mutable_repository.relative_path) }
- let(:mutable_repository_rugged) { Rugged::Repository.new(mutable_repository_path) }
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
- let(:repository_rugged) { Rugged::Repository.new(repository_path) }
- let(:storage_path) { TestEnv.repos_path }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository.raw }
+
+ let(:mutable_project) { create(:project, :repository) }
+ let(:mutable_repository) { mutable_project.repository.raw }
let(:user) { build(:user) }
describe "Respond to" do
@@ -61,8 +59,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#branch_names' do
subject { repository.branch_names }
- it 'has SeedRepo::Repo::BRANCHES.size elements' do
- expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size)
+ it 'has TestRepo::BRANCH_SHA.size elements' do
+ expect(subject.size).to eq(TestEnv::BRANCH_SHA.size)
end
it 'returns UTF-8' do
@@ -85,8 +83,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it { is_expected.to be_kind_of Array }
- it 'has SeedRepo::Repo::TAGS.size elements' do
- expect(subject.size).to eq(SeedRepo::Repo::TAGS.size)
+ it 'has some elements' do
+ expect(subject.size).to be >= 1
end
it 'returns UTF-8' do
@@ -96,63 +94,24 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#last' do
subject { super().last }
- it { is_expected.to eq("v1.2.1") }
+ it { is_expected.to eq("v1.1.1") }
end
+
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
- it 'gets the tag names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names)
- subject
- end
-
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names
end
describe '#tags' do
subject { repository.tags }
- it 'gets tags from GitalyClient' do
- expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
- expect(service).to receive(:tags)
- end
-
- subject
- end
-
- context 'with sorting option' do
- subject { repository.tags(sort_by: 'name_asc') }
-
- it 'gets tags from GitalyClient' do
- expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
- expect(service).to receive(:tags).with(sort_by: 'name_asc', pagination_params: nil)
- end
-
- subject
- end
- end
-
- context 'with pagination option' do
- subject { repository.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }) }
-
- it 'gets tags from GitalyClient' do
- expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
- expect(service).to receive(:tags).with(
- sort_by: nil,
- pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }
- )
- end
-
- subject
- end
- end
-
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tags
end
describe '#archive_metadata' do
let(:storage_path) { '/tmp' }
- let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) }
+ let(:cache_key) { File.join(repository.gl_repository, TestEnv::BRANCH_SHA['master']) }
let(:append_sha) { true }
let(:ref) { 'master' }
@@ -162,12 +121,12 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:expected_extension) { 'tar.gz' }
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) }
- let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
+ let(:expected_prefix) { "gitlab-git-test-#{ref}-#{TestEnv::BRANCH_SHA['master']}" }
subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
it 'sets CommitId to the commit SHA' do
- expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
+ expect(metadata['CommitId']).to start_with(TestEnv::BRANCH_SHA['master'])
end
it 'sets ArchivePrefix to the expected prefix' do
@@ -175,7 +134,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
it 'sets ArchivePath to the expected globally-unique path' do
- expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
+ expect(expected_path).to include(File.join(repository.gl_repository, TestEnv::BRANCH_SHA['master']))
expect(metadata['ArchivePath']).to eq(expected_path)
end
@@ -190,7 +149,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
context 'append_sha varies archive path and filename' do
where(:append_sha, :ref, :expected_prefix) do
- sha = SeedRepo::LastCommit::ID
+ sha = TestEnv::BRANCH_SHA['master']
true | 'master' | "gitlab-git-test-master-#{sha}"
true | sha | "gitlab-git-test-#{sha}-#{sha}"
@@ -224,13 +183,13 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#size' do
subject { repository.size }
- it { is_expected.to be < 2 }
+ it { is_expected.to be > 0 }
end
describe '#to_s' do
subject { repository.to_s }
- it { is_expected.to eq("<Gitlab::Git::Repository: group/project>") }
+ it { is_expected.to eq("<Gitlab::Git::Repository: #{project.full_path}>") }
end
describe '#object_directory_size' do
@@ -259,26 +218,25 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#first' do
subject { super().first }
- it { is_expected.to eq('feature') }
+ it { is_expected.to eq(TestEnv::BRANCH_SHA.keys.min) }
end
describe '#last' do
subject { super().last }
- it { is_expected.to eq('v1.2.1') }
+ it { is_expected.to eq('v1.1.1') }
end
end
describe '#submodule_url_for' do
- let(:ref) { 'master' }
+ let(:ref) { 'submodule_inside_folder' }
def submodule_url(path)
repository.submodule_url_for(ref, path)
end
it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') }
- it { expect(submodule_url('nested/six')).to eq('git://github.com/randx/six.git') }
- it { expect(submodule_url('deeper/nested/six')).to eq('git://github.com/randx/six.git') }
+ it { expect(submodule_url('test_inside_folder/another_folder/six')).to eq('git://github.com/randx/six.git') }
it { expect(submodule_url('invalid/path')).to eq(nil) }
context 'uncommitted submodule dir' do
@@ -288,7 +246,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'tags' do
- let(:ref) { 'v1.2.1' }
+ let(:ref) { 'v1.1.1' }
it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') }
end
@@ -313,17 +271,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
urls = repository.submodule_urls_for(ref)
expect(urls).to eq({
- "deeper/nested/six" => "git://github.com/randx/six.git",
- "gitlab-grack" => "https://gitlab.com/gitlab-org/gitlab-grack.git",
- "gitlab-shell" => "https://github.com/gitlabhq/gitlab-shell.git",
- "nested/six" => "git://github.com/randx/six.git",
+ "gitlab-grack" => "https://gitlab.com/gitlab-org/gitlab-grack.git",
+ "gitlab-shell" => "https://github.com/gitlabhq/gitlab-shell.git",
"six" => "git://github.com/randx/six.git"
})
end
end
describe '#commit_count' do
- it { expect(repository.commit_count("master")).to eq(25) }
+ it { expect(repository.commit_count("master")).to eq(37) }
it { expect(repository.commit_count("feature")).to eq(9) }
it { expect(repository.commit_count("does-not-exist")).to eq(0) }
@@ -353,7 +309,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch('right-branch')
left.times do |i|
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: 'left-branch',
message: 'some more content for a',
@@ -366,7 +322,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
right.times do |i|
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: 'right-branch',
message: 'some more content for b',
@@ -411,7 +367,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch('right-branch')
left.times do |i|
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: 'left-branch',
message: 'some more content for a',
@@ -424,7 +380,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
right.times do |i|
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: 'right-branch',
message: 'some more content for b',
@@ -461,47 +417,32 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#has_local_branches?' do
context 'check for local branches' do
it { expect(repository.has_local_branches?).to eq(true) }
+ end
+ end
- context 'mutable' do
- let(:repository) { mutable_repository }
+ describe '#delete_branch' do
+ let(:repository) { mutable_repository }
- after do
- ensure_seeds
- end
+ it 'deletes a branch' do
+ expect(repository.find_branch('feature')).not_to be_nil
- it 'returns false when there are no branches' do
- # Sanity check
- expect(repository.has_local_branches?).to eq(true)
+ repository.delete_branch('feature')
- FileUtils.rm_rf(File.join(repository_path, 'packed-refs'))
- heads_dir = File.join(repository_path, 'refs/heads')
- FileUtils.rm_rf(heads_dir)
- FileUtils.mkdir_p(heads_dir)
+ expect(repository.find_branch('feature')).to be_nil
+ end
- repository.expire_has_local_branches_cache
- expect(repository.has_local_branches?).to eq(false)
- end
- end
+ it 'deletes a fully qualified branch' do
+ expect(repository.find_branch('feature')).not_to be_nil
- context 'memoizes the value' do
- it 'returns true' do
- expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original
+ repository.delete_branch('refs/heads/feature')
- 2.times do
- expect(repository.has_local_branches?).to eq(true)
- end
- end
- end
+ expect(repository.find_branch('feature')).to be_nil
end
end
describe '#delete_refs' do
let(:repository) { mutable_repository }
- after do
- ensure_seeds
- end
-
it 'deletes the ref' do
repository.delete_refs('refs/heads/feature')
@@ -548,9 +489,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
subject { repository.refs_hash }
it "has as many entries as branches and tags" do
- expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
# We flatten in case a commit is pointed at by more than one branch and/or tag
- expect(subject.values.flatten.size).to eq(expected_refs.size)
+ expect(subject.values.flatten.size).to be > 0
end
it 'has valid commit ids as keys' do
@@ -598,7 +538,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
before do
repository.create_branch(ref)
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: ref,
message: 'committing something',
@@ -608,7 +548,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
content: content
}]
)
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: ref,
message: 'committing something',
@@ -620,10 +560,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
)
end
- after do
- ensure_seeds
- end
-
subject do
repository.search_files_by_content(content, ref)
end
@@ -647,8 +583,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:filter) { 'files\/.*\/.*\.rb' }
it 'returns matched files' do
- expect(result).to contain_exactly('files/links/regex.rb',
- 'files/ruby/popen.rb',
+ expect(result).to contain_exactly('files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/ruby/version_info.rb')
end
@@ -673,6 +608,61 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#search_files_by_name' do
+ let(:ref) { 'master' }
+
+ subject(:result) { mutable_repository.search_files_by_name(query, ref) }
+
+ context 'when sending a valid name' do
+ let(:query) { 'files/ruby/popen.rb' }
+
+ it 'returns matched files' do
+ expect(result).to contain_exactly('files/ruby/popen.rb')
+ end
+ end
+
+ context 'when sending a name with space' do
+ let(:query) { 'file with space.md' }
+
+ before do
+ mutable_repository.commit_files(
+ user,
+ actions: [{ action: :create, file_path: "file with space.md", content: "Test content" }],
+ branch_name: ref, message: "Test"
+ )
+ end
+
+ it 'returns matched files' do
+ expect(result).to contain_exactly('file with space.md')
+ end
+ end
+
+ context 'when sending a name with special ASCII characters' do
+ let(:file_name) { 'Hello !@#$%^&*()' }
+ let(:query) { file_name }
+
+ before do
+ mutable_repository.commit_files(
+ user,
+ actions: [{ action: :create, file_path: file_name, content: "Test content" }],
+ branch_name: ref, message: "Test"
+ )
+ end
+
+ it 'returns matched files' do
+ expect(result).to contain_exactly(file_name)
+ end
+ end
+
+ context 'when sending a non-existing name' do
+ let(:query) { 'please do not exist.md' }
+
+ it 'raises error' do
+ expect(result).to eql([])
+ end
+ end
+ end
+
describe '#find_remote_root_ref' do
it 'gets the remote root ref from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
@@ -720,7 +710,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
before do
# Add new commits so that there's a renamed file in the commit history
- @commit_with_old_name_id = repository.multi_action(
+ @commit_with_old_name_id = repository.commit_files(
user,
branch_name: repository.root_ref,
message: 'Update CHANGELOG',
@@ -730,7 +720,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
content: 'CHANGELOG'
}]
).newrev
- @rename_commit_id = repository.multi_action(
+ @rename_commit_id = repository.commit_files(
user,
branch_name: repository.root_ref,
message: 'Move CHANGELOG to encoding/',
@@ -741,7 +731,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
content: 'CHANGELOG'
}]
).newrev
- @commit_with_new_name_id = repository.multi_action(
+ @commit_with_new_name_id = repository.commit_files(
user,
branch_name: repository.root_ref,
message: 'Edit encoding/CHANGELOG',
@@ -755,7 +745,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
after do
# Erase our commits so other tests get the original repo
- repository.write_ref(repository.root_ref, SeedRepo::LastCommit::ID)
+ repository.write_ref(repository.root_ref, TestEnv::BRANCH_SHA['master'])
end
context "where 'follow' == true" do
@@ -908,16 +898,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
expect(log_commits).not_to include(commit_with_new_name)
end
end
-
- context "and 'path' includes a directory that used to be a file" do
- let(:log_commits) do
- repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
- end
-
- it "returns a list of commits" do
- expect(log_commits.size).to eq(1)
- end
- end
end
context "where provides 'after' timestamp" do
@@ -981,7 +961,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'returns a list of commits' do
commits = repository.log({ all: true, limit: 50 })
- expect(commits.size).to eq(37)
+ expect(commits.size).to eq(50)
end
end
end
@@ -992,7 +972,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe '#blobs' do
- let_it_be(:commit_oid) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let_it_be(:commit_oid) { TestEnv::BRANCH_SHA['master'] }
shared_examples 'a blob enumeration' do
it 'enumerates blobs' do
@@ -1008,7 +988,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
context 'single revision' do
let(:revisions) { [commit_oid] }
- let(:expected_blobs) { 53 }
+ let(:expected_blobs) { 52 }
it_behaves_like 'a blob enumeration'
end
@@ -1038,48 +1018,31 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it_behaves_like 'a blob enumeration'
end
-
- context 'partially blank revisions' do
- let(:revisions) { [::Gitlab::Git::BLANK_SHA, commit_oid] }
- let(:expected_blobs) { 53 }
-
- before do
- expect_next_instance_of(Gitlab::GitalyClient::BlobService) do |service|
- expect(service)
- .to receive(:list_blobs)
- .with([commit_oid], kind_of(Hash))
- .and_call_original
- end
- end
-
- it_behaves_like 'a blob enumeration'
- end
end
describe '#new_blobs' do
let(:repository) { mutable_repository }
- let(:repository_rugged) { mutable_repository_rugged }
- let(:blob) { create_blob('This is a new blob') }
- let(:commit) { create_commit('nested/new-blob.txt' => blob) }
-
- def create_blob(content)
- repository_rugged.write(content, :blob)
- end
+ let(:commit) { create_commit('nested/new-blob.txt' => 'This is a new blob') }
def create_commit(blobs)
- author = { name: 'Test User', email: 'mail@example.com', time: Time.now }
+ commit_result = repository.commit_files(
+ user,
+ branch_name: 'a-new-branch',
+ message: 'Add a file',
+ actions: blobs.map do |path, content|
+ {
+ action: :create,
+ file_path: path,
+ content: content
+ }
+ end
+ )
- index = repository_rugged.index
- blobs.each do |path, oid|
- index.add(path: path, oid: oid, mode: 0100644)
- end
+ # new_blobs only returns unreferenced blobs because it is used for hooks.
+ # Gitaly does not allow us to create loose objects via the RPC.
+ repository.delete_branch('a-new-branch')
- Rugged::Commit.create(repository_rugged,
- author: author,
- committer: author,
- message: "Message",
- parents: [],
- tree: index.write_tree(repository_rugged))
+ commit_result.newrev
end
subject { repository.new_blobs(newrevs).to_a }
@@ -1112,7 +1075,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:newrevs) { commit }
let(:expected_newrevs) { ['--not', '--all', '--not', newrevs] }
let(:expected_blobs) do
- [have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)]
+ [have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18)]
end
it_behaves_like '#new_blobs with revisions'
@@ -1122,20 +1085,19 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:newrevs) { [commit] }
let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs }
let(:expected_blobs) do
- [have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)]
+ [have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18)]
end
it_behaves_like '#new_blobs with revisions'
end
context 'with multiple revisions' do
- let(:another_blob) { create_blob('Another blob') }
- let(:newrevs) { [commit, create_commit('another_path.txt' => another_blob)] }
+ let(:newrevs) { [commit, create_commit('another_path.txt' => 'Another blob')] }
let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs.sort }
let(:expected_blobs) do
[
- have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18),
- have_attributes(class: Gitlab::Git::Blob, id: another_blob, path: 'another_path.txt', size: 12)
+ have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18),
+ have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'another_path.txt', size: 12)
]
end
@@ -1147,7 +1109,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:expected_newrevs) { ['--not', '--all', '--not', commit] }
let(:expected_blobs) do
[
- have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)
+ have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18)
]
end
@@ -1159,7 +1121,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:expected_newrevs) { ['--not', '--all', '--not', commit] }
let(:expected_blobs) do
[
- have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)
+ have_attributes(class: Gitlab::Git::Blob, id: an_instance_of(String), path: 'nested/new-blob.txt', size: 18)
]
end
@@ -1212,14 +1174,22 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#new_commits' do
let(:repository) { mutable_repository }
let(:new_commit) do
- author = { name: 'Test User', email: 'mail@example.com', time: Time.now }
+ commit_result = repository.commit_files(
+ user,
+ branch_name: 'a-new-branch',
+ message: 'Message',
+ actions: [{
+ action: :create,
+ file_path: 'some_file.txt',
+ content: 'This is a file'
+ }]
+ )
+
+ # new_commits only returns unreferenced commits because it is used for
+ # hooks. Gitaly does not allow us to create loose objects via the RPC.
+ repository.delete_branch('a-new-branch')
- Rugged::Commit.create(repository_rugged,
- author: author,
- committer: author,
- message: "Message",
- parents: [],
- tree: "4b825dc642cb6eb9a060e54bf8d69288fbee4904")
+ commit_result.newrev
end
let(:expected_commits) { 1 }
@@ -1248,7 +1218,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#count_commits_between' do
subject { repository.count_commits_between('feature', 'master') }
- it { is_expected.to eq(17) }
+ it { is_expected.to eq(29) }
end
describe '#raw_changes_between' do
@@ -1275,26 +1245,26 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
- context 'with valid revs' do
- let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
- let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ context 'with valid revs', :aggregate_failures do
+ let(:old_rev) { TestEnv::BRANCH_SHA['feature'] }
+ let(:new_rev) { TestEnv::BRANCH_SHA['master'] }
it 'returns the changes' do
- expect(changes.size).to eq(9)
- expect(changes.first.operation).to eq(:modified)
- expect(changes.first.new_path).to eq('.gitmodules')
+ expect(changes.size).to eq(21)
+ expect(changes.first.operation).to eq(:deleted)
+ expect(changes.first.old_path).to eq('.DS_Store')
expect(changes.last.operation).to eq(:added)
- expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ expect(changes.last.new_path).to eq('with space/README.md')
end
end
end
describe '#merge_base' do
where(:from, :to, :result) do
- '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
- '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
- '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil
- 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil
+ 'master' | 'feature' | 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f'
+ 'feature' | 'master' | 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f'
+ 'master' | 'foobar' | nil
+ 'foobar' | 'master' | nil
end
with_them do
@@ -1308,7 +1278,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'returns the number of commits after timestamp' do
options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') }
- expect(repository.count_commits(options)).to eq(25)
+ expect(repository.count_commits(options)).to eq(37)
end
end
@@ -1337,28 +1307,28 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'with option :from and option :to' do
- it 'returns the number of commits ahead for fix-mode..fix-blob-path' do
- options = { from: 'fix-mode', to: 'fix-blob-path' }
+ it 'returns the number of commits ahead for master..feature' do
+ options = { from: 'master', to: 'feature' }
- expect(repository.count_commits(options)).to eq(2)
+ expect(repository.count_commits(options)).to eq(1)
end
- it 'returns the number of commits ahead for fix-blob-path..fix-mode' do
- options = { from: 'fix-blob-path', to: 'fix-mode' }
+ it 'returns the number of commits ahead for feature..master' do
+ options = { from: 'feature', to: 'master' }
- expect(repository.count_commits(options)).to eq(1)
+ expect(repository.count_commits(options)).to eq(29)
end
context 'with option :left_right' do
- it 'returns the number of commits for fix-mode...fix-blob-path' do
- options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true }
+ it 'returns the number of commits for master..feature' do
+ options = { from: 'master', to: 'feature', left_right: true }
- expect(repository.count_commits(options)).to eq([1, 2])
+ expect(repository.count_commits(options)).to eq([29, 1])
end
context 'with max_count' do
- it 'returns the number of commits with path' do
- options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 }
+ it 'returns the number of commits' do
+ options = { from: 'feature', to: 'master', left_right: true, max_count: 1 }
expect(repository.count_commits(options)).to eq([1, 1])
end
@@ -1378,7 +1348,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it "returns the number of commits in the whole repository" do
options = { all: true }
- expect(repository.count_commits(options)).to eq(34)
+ expect(repository.count_commits(options)).to eq(314)
end
end
@@ -1416,10 +1386,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch('local_branch')
end
- after do
- ensure_seeds
- end
-
it 'returns the local and remote branches' do
expect(subject.any? { |b| b.name == 'joe/remote_branch' }).to eq(true)
expect(subject.any? { |b| b.name == 'local_branch' }).to eq(true)
@@ -1431,7 +1397,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#branch_count' do
it 'returns the number of branches' do
- expect(repository.branch_count).to eq(11)
+ expect(repository.branch_count).to eq(TestEnv::BRANCH_SHA.size)
end
context 'with local and remote branches' do
@@ -1442,10 +1408,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch('local_branch')
end
- after do
- ensure_seeds
- end
-
it 'returns the count of local branches' do
expect(repository.branch_count).to eq(repository.local_branches.count)
end
@@ -1488,21 +1450,16 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'when no branch names are specified' do
+ let(:repository) { mutable_repository }
+
before do
repository.create_branch('identical')
end
- after do
- ensure_seeds
- end
-
it 'returns all merged branch names except for identical one' do
names = repository.merged_branch_names
- expect(names).to include('merge-test')
- expect(names).to include('fix-mode')
- expect(names).not_to include('feature')
- expect(names).not_to include('identical')
+ expect(names).to match_array(["'test'", "branch-merged", "flatten-dir", "improve/awesome", "merge-test"])
end
end
end
@@ -1556,24 +1513,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe '#find_changed_paths' do
- let(:commit_1) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
- let(:commit_2) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let(:commit_1) { TestEnv::BRANCH_SHA['with-executables'] }
+ let(:commit_2) { TestEnv::BRANCH_SHA['master'] }
let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:commit_1_files) do
- [
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls"),
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/touch"),
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/regex.rb"),
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/ruby-style-guide.md"),
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/touch"),
- Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"),
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "deeper/nested/six"),
- Gitlab::Git::ChangedPath.new(status: :ADDED, path: "nested/six")
- ]
+ [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls")]
end
let(:commit_2_files) do
- [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bin/executable")]
+ [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bar/branch-test.txt")]
end
let(:commit_3_files) do
@@ -1621,7 +1569,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
it "read every file paths of master branch" do
- expect(master_file_paths.length).to equal(40)
+ expect(master_file_paths.length).to equal(38)
end
it "reads full file paths of master branch" do
@@ -1646,11 +1594,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe "#copy_gitattributes" do
- let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
-
- after do
- FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
- end
+ let(:repository) { mutable_repository }
it "raises an error with invalid ref" do
expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
@@ -1673,63 +1617,10 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository
end
end
-
- context "with no .gitattrbutes" do
- before do
- repository.copy_gitattributes("master")
- end
-
- it "does not have an info/attributes" do
- expect(File.exist?(attributes_path)).to be_falsey
- end
- end
-
- context "with .gitattrbutes" do
- before do
- repository.copy_gitattributes("gitattributes")
- end
-
- it "has an info/attributes" do
- expect(File.exist?(attributes_path)).to be_truthy
- end
-
- it "has the same content in info/attributes as .gitattributes" do
- contents = File.open(attributes_path, "rb") { |f| f.read }
- expect(contents).to eq("*.md binary\n")
- end
- end
-
- context "with updated .gitattrbutes" do
- before do
- repository.copy_gitattributes("gitattributes")
- repository.copy_gitattributes("gitattributes-updated")
- end
-
- it "has an info/attributes" do
- expect(File.exist?(attributes_path)).to be_truthy
- end
-
- it "has the updated content in info/attributes" do
- contents = File.read(attributes_path)
- expect(contents).to eq("*.txt binary\n")
- end
- end
-
- context "with no .gitattrbutes in HEAD but with previous info/attributes" do
- before do
- repository.copy_gitattributes("gitattributes")
- repository.copy_gitattributes("master")
- end
-
- it "does not have an info/attributes" do
- expect(File.exist?(attributes_path)).to be_falsey
- end
- end
end
describe '#gitattribute' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
+ let(:repository) { mutable_repository }
context 'with gitattributes' do
before do
@@ -1808,10 +1699,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch('local_branch')
end
- after do
- ensure_seeds
- end
-
it 'returns the local branches' do
expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
@@ -1880,7 +1767,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#languages' do
it 'returns exactly the expected results' do
- languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6')
+ languages = repository.languages(TestEnv::BRANCH_SHA['master'])
expect(languages).to match_array([
{ value: a_value_within(0.1).of(66.7), label: "Ruby", color: "#701516", highlight: "#701516" },
@@ -1918,18 +1805,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#fetch_source_branch!' do
let(:local_ref) { 'refs/merge-requests/1/head' }
+ let(:repository) { create(:project, :repository).repository.raw }
let(:source_repository) { mutable_repository }
- after do
- ensure_seeds
- end
-
context 'when the branch exists' do
context 'when the commit does not exist locally' do
let(:source_branch) { 'new-branch-for-fetch-source-branch' }
let!(:new_oid) do
- source_repository.multi_action(
+ source_repository.commit_files(
user,
branch_name: source_branch,
message: 'Add a file',
@@ -1949,14 +1833,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
context 'when the commit exists locally' do
let(:source_branch) { 'master' }
- let(:expected_oid) { SeedRepo::LastCommit::ID }
+ let(:expected_oid) { TestEnv::BRANCH_SHA['master'] }
it 'writes the ref' do
# Sanity check: the commit should already exist
expect(repository.commit(expected_oid)).not_to be_nil
expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
- expect(repository.commit(local_ref).sha).to eq(expected_oid)
+ expect(repository.commit(local_ref).sha).to start_with(expected_oid)
end
end
end
@@ -2012,9 +1896,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
it 'writes other refs' do
- repository.write_ref('refs/heads/feature', SeedRepo::Commit::ID)
+ repository.write_ref('refs/heads/feature', TestEnv::BRANCH_SHA['master'])
- expect(repository.commit('feature').sha).to eq(SeedRepo::Commit::ID)
+ expect(repository.commit('feature').sha).to start_with(TestEnv::BRANCH_SHA['master'])
end
end
@@ -2052,28 +1936,28 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'returns nil for an empty repo' do
project = create(:project)
- expect(project.repository.refs_by_oid(oid: SeedRepo::Commit::ID, limit: 0)).to be_nil
+ expect(project.repository.refs_by_oid(oid: TestEnv::BRANCH_SHA['master'], limit: 0)).to be_nil
end
end
describe '#set_full_path' do
+ let(:full_path) { 'some/path' }
+
before do
- repository.set_full_path(full_path: repository_path)
+ repository.set_full_path(full_path: full_path)
end
- context 'is given a path' do
- it 'writes it to disk' do
- repository.set_full_path(full_path: "not-the/real-path.git")
+ it 'writes full_path to gitaly' do
+ repository.set_full_path(full_path: "not-the/real-path.git")
- expect(repository.full_path).to eq('not-the/real-path.git')
- end
+ expect(repository.full_path).to eq('not-the/real-path.git')
end
context 'it is given an empty path' do
it 'does not write it to disk' do
repository.set_full_path(full_path: "")
- expect(repository.full_path).to eq(repository_path)
+ expect(repository.full_path).to eq(full_path)
end
end
@@ -2145,10 +2029,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f')
end
- after do
- ensure_seeds
- end
-
it 'can perform a merge' do
merge_commit_id = nil
result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id|
@@ -2185,10 +2065,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.create_branch(target_branch, branch_head)
end
- after do
- ensure_seeds
- end
-
subject { repository.ff_merge(user, source_sha, target_branch) }
shared_examples '#ff_merge' do
@@ -2242,14 +2118,10 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:repository) { mutable_repository }
before do
- repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
- repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1")
- repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae")
- repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
- end
-
- after do
- ensure_seeds
+ repository.write_ref("refs/delete/a", TestEnv::BRANCH_SHA['master'])
+ repository.write_ref("refs/also-delete/b", TestEnv::BRANCH_SHA['master'])
+ repository.write_ref("refs/keep/c", TestEnv::BRANCH_SHA['master'])
+ repository.write_ref("refs/also-keep/d", TestEnv::BRANCH_SHA['master'])
end
it 'deletes all refs except those with the specified prefixes' do
@@ -2272,11 +2144,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'saves a bundle to disk' do
repository.bundle_to_disk(save_path)
- success = system(
- *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}),
- [:out, :err] => '/dev/null'
- )
- expect(success).to be true
+ expect(File).to exist(save_path)
end
end
@@ -2326,41 +2194,22 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#checksum' do
it 'calculates the checksum for non-empty repo' do
- expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059'
- end
-
- it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
- FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
-
- system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
- chdir: storage_path,
- out: '/dev/null',
- err: '/dev/null')
-
- empty_repo = described_class.new('default', 'empty-repo.git', '', 'group/empty-repo')
-
- expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
+ expect(repository.checksum.length).to be(40)
+ expect(Gitlab::Git.blank_ref?(repository.checksum)).to be false
end
- it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
- FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
-
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
-
- File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
+ it 'returns a blank sha for an empty repo' do
+ repository = create(:project, :empty_repo).repository
- non_valid = described_class.new('default', 'non-valid.git', '', 'a/non-valid')
-
- expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
+ expect(Gitlab::Git.blank_ref?(repository.checksum)).to be true
end
- it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
- broken_repo = described_class.new('default', 'a/path.git', '', 'a/path')
+ it 'raises NoRepository for a non-existent repo' do
+ repository = create(:project).repository
- expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ expect do
+ repository.checksum
+ end.to raise_error(described_class::NoRepository)
end
end
@@ -2375,7 +2224,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#squash' do
let(:branch_name) { 'fix' }
- let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let(:start_sha) { TestEnv::BRANCH_SHA['master'] }
let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }
subject do
@@ -2412,7 +2261,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
context 'when the diff contains a rename' do
let(:end_sha) do
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: repository.root_ref,
message: 'Move CHANGELOG to encoding/',
@@ -2427,7 +2276,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
after do
# Erase our commits so other tests get the original repo
- repository.write_ref(repository.root_ref, SeedRepo::LastCommit::ID)
+ repository.write_ref(repository.root_ref, TestEnv::BRANCH_SHA['master'])
end
it 'does not include the renamed file in the sparse checkout' do
@@ -2480,9 +2329,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe '#disconnect_alternates' do
- let(:project) { create(:project, :repository) }
+ let(:project) { mutable_project }
+ let(:repository) { mutable_repository }
let(:pool_repository) { create(:pool_repository) }
- let(:repository) { project.repository }
let(:object_pool) { pool_repository.object_pool }
before do
@@ -2495,7 +2344,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'can still access objects in the object pool' do
object_pool.link(repository)
- new_commit_id = object_pool.repository.multi_action(
+ new_commit_id = object_pool.repository.commit_files(
project.owner,
branch_name: object_pool.repository.root_ref,
message: 'Add a file',
@@ -2515,8 +2364,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe '#rename' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
+ let(:repository) { mutable_repository }
it 'moves the repository' do
checksum = repository.checksum
@@ -2531,15 +2379,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe '#remove' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
+ let(:repository) { mutable_repository }
it 'removes the repository' do
expect(repository.exists?).to be true
repository.remove
- expect(repository.raw_repository.exists?).to be false
+ expect(repository.exists?).to be false
end
context 'when the repository does not exist' do
@@ -2550,15 +2397,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
repository.remove
- expect(repository.raw_repository.exists?).to be false
+ expect(repository.exists?).to be false
end
end
end
describe '#import_repository' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:repository) { create(:project).repository }
- let(:repository) { project.repository }
let(:url) { 'http://invalid.invalid' }
it 'raises an error if a relative path is provided' do
@@ -2584,11 +2430,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
describe '#replicate' do
let(:new_repository) do
- Gitlab::Git::Repository.new('test_second_storage', TEST_REPO_PATH, '', 'group/project')
+ Gitlab::Git::Repository.new('test_second_storage', repository.relative_path, '', 'group/project')
end
- let(:new_repository_path) { File.join(TestEnv::SECOND_STORAGE_PATH, new_repository.relative_path) }
-
subject { new_repository.replicate(repository) }
before do
@@ -2622,7 +2466,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'with keep-around refs' do
- let(:sha) { SeedRepo::Commit::ID }
+ let(:repository) { mutable_repository }
+ let(:sha) { TestEnv::BRANCH_SHA['master'] }
let(:keep_around_ref) { "refs/keep-around/#{sha}" }
let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
index 03d1c125e36..747611a59e6 100644
--- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
+++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'json'
require 'tempfile'
-RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
+RSpec.describe Gitlab::Git::RuggedImpl::UseRugged do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 2e4520cd3a0..7c84c737c00 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe Gitlab::Git::Tree do
let(:subdir_file) { entries.first }
# rubocop: enable Rails/FindBy
let!(:sha) do
- repository.multi_action(
+ repository.commit_files(
user,
branch_name: 'HEAD',
message: "Create #{filename}",
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
index a0237c821b5..dd925a902ab 100644
--- a/spec/lib/gitlab/git/util_spec.rb
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Git::Util do
describe '#count_lines' do
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index dddcf8c40fc..05c7ac149e4 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -8,9 +8,15 @@ RSpec.describe Gitlab::Git::Wiki do
let(:project) { create(:project) }
let(:user) { project.first_owner }
let(:project_wiki) { ProjectWiki.new(project, user) }
+ let(:repository) { project_wiki.repository }
+ let(:default_branch) { described_class.default_ref(project) }
subject(:wiki) { project_wiki.wiki }
+ before do
+ repository.create_if_not_exists(project_wiki.default_branch)
+ end
+
describe '#pages' do
before do
create_page('page1', 'content')
@@ -44,7 +50,7 @@ RSpec.describe Gitlab::Git::Wiki do
after do
destroy_page('page1')
- destroy_page('page1', 'foo')
+ destroy_page('foo/page1')
end
it 'returns the right page' do
@@ -71,20 +77,20 @@ RSpec.describe Gitlab::Git::Wiki do
end
describe '#preview_slug' do
- where(:title, :format, :expected_slug) do
- 'The Best Thing' | :markdown | 'The-Best-Thing'
- 'The Best Thing' | :md | 'The-Best-Thing'
- 'The Best Thing' | :txt | 'The-Best-Thing'
- 'A Subject/Title Here' | :txt | 'A-Subject/Title-Here'
- 'A subject' | :txt | 'A-subject'
- 'A 1/B 2/C 3' | :txt | 'A-1/B-2/C-3'
- 'subject/title' | :txt | 'subject/title'
- 'subject/title.md' | :txt | 'subject/title.md'
- 'foo<bar>+baz' | :txt | 'foo-bar--baz'
- 'foo%2Fbar' | :txt | 'foo%2Fbar'
- '' | :markdown | '.md'
- '' | :md | '.md'
- '' | :txt | '.txt'
+ where(:title, :file_extension, :format, :expected_slug) do
+ 'The Best Thing' | :md | :markdown | 'The-Best-Thing'
+ 'The Best Thing' | :md | :md | 'The-Best-Thing'
+ 'The Best Thing' | :txt | :txt | 'The-Best-Thing'
+ 'A Subject/Title Here' | :txt | :txt | 'A-Subject/Title-Here'
+ 'A subject' | :txt | :txt | 'A-subject'
+ 'A 1/B 2/C 3' | :txt | :txt | 'A-1/B-2/C-3'
+ 'subject/title' | :txt | :txt | 'subject/title'
+ 'subject/title.md' | :txt | :txt | 'subject/title.md'
+ 'foo<bar>+baz' | :txt | :txt | 'foo-bar--baz'
+ 'foo%2Fbar' | :txt | :txt | 'foo%2Fbar'
+ '' | :md | :markdown | '.md'
+ '' | :md | :md | '.md'
+ '' | :txt | :txt | '.txt'
end
with_them do
@@ -97,7 +103,7 @@ RSpec.describe Gitlab::Git::Wiki do
it 'matches the slug generated by gitaly' do
skip('Gitaly cannot generate a slug for an empty title') unless title.present?
- create_page(title, 'content', format: format)
+ create_page(title, 'content', file_extension)
gitaly_slug = wiki.list_pages.first.url_path
@@ -106,16 +112,23 @@ RSpec.describe Gitlab::Git::Wiki do
end
end
- def create_page(name, content, format: :markdown)
- wiki.write_page(name, format, content, commit_details(name))
- end
-
- def commit_details(name)
- Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}")
+ def create_page(name, content, extension = :md)
+ repository.create_file(
+ user, ::Wiki.sluggified_full_path(name, extension.to_s), content,
+ branch_name: default_branch,
+ message: "created page #{name}",
+ author_email: user.email,
+ author_name: user.name
+ )
end
- def destroy_page(title, dir = '')
- page = wiki.page(title: title, dir: dir)
- project_wiki.delete_page(page, "test commit")
+ def destroy_page(name, extension = :md)
+ repository.delete_file(
+ user, ::Wiki.sluggified_full_path(name, extension.to_s),
+ branch_name: described_class.default_ref(project),
+ message: "delete page #{name}",
+ author_email: user.email,
+ author_name: user.name
+ )
end
end
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index f359679a930..0f6ef55b4b1 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -7,10 +7,18 @@ RSpec.describe Gitlab::Git do
let(:committer_name) { 'John Doe' }
describe '.ref_name' do
+ let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" }
+
it 'ensure ref is a valid UTF-8 string' do
- utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5"
+ expect(described_class.ref_name(ref)).to eq("an_invalid_ref_%E5")
+ end
- expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_å")
+ context 'when ref contains characters \x80 - \xFF' do
+ let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + "\x90" }
+
+ it 'correctly converts it' do
+ expect(described_class.ref_name(ref)).to eq("%90")
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index ed6a87cda6f..ff3cade07c0 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -297,6 +297,11 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
describe '#list_commits' do
let(:revisions) { 'master' }
let(:reverse) { false }
+ let(:author) { nil }
+ let(:ignore_case) { nil }
+ let(:commit_message_patterns) { nil }
+ let(:before) { nil }
+ let(:after) { nil }
let(:pagination_params) { nil }
shared_examples 'a ListCommits request' do
@@ -309,13 +314,18 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
expected_request = gitaly_request_with_params(
Array.wrap(revisions),
reverse: reverse,
+ author: author,
+ ignore_case: ignore_case,
+ commit_message_patterns: commit_message_patterns,
+ before: before,
+ after: after,
pagination_params: pagination_params
)
expect(service).to receive(:list_commits).with(expected_request, kind_of(Hash)).and_return([])
end
- client.list_commits(revisions, reverse: reverse, pagination_params: pagination_params)
+ client.list_commits(revisions, { reverse: reverse, author: author, ignore_case: ignore_case, commit_message_patterns: commit_message_patterns, before: before, after: after, pagination_params: pagination_params })
end
end
@@ -333,7 +343,12 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
it_behaves_like 'a ListCommits request'
end
- context 'with pagination params' do
+ context 'with commit message, author, before and after' do
+ let(:author) { "Dmitriy" }
+ let(:before) { 1474828200 }
+ let(:after) { 1474828200 }
+ let(:commit_message_patterns) { "Initial commit" }
+ let(:ignore_case) { true }
let(:pagination_params) { { limit: 1, page_token: 'foo' } }
it_behaves_like 'a ListCommits request'
diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb
index 230322faecd..2c1f684c0c5 100644
--- a/spec/lib/gitlab/gitaly_client/diff_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GitalyClient::Diff do
let(:diff_fields) do
diff --git a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
index 54c84ddc56f..39fd752ef7f 100644
--- a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GitalyClient::DiffStitcher do
describe 'enumeration' do
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 5d854f0c9d1..7e8aaa3cdf4 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -56,6 +56,85 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
Gitlab::Git::PreReceiveError, "something failed")
end
end
+
+ context 'with structured errors' do
+ context 'with CustomHookError' do
+ let(:stdout) { nil }
+ let(:stderr) { nil }
+ let(:error_message) { "error_message" }
+
+ let(:custom_hook_error) do
+ new_detailed_error(
+ GRPC::Core::StatusCodes::PERMISSION_DENIED,
+ error_message,
+ Gitaly::UserCreateBranchError.new(
+ custom_hook: Gitaly::CustomHookError.new(
+ stdout: stdout,
+ stderr: stderr,
+ hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE
+ )))
+ end
+
+ shared_examples 'failed branch creation' do
+ it 'raised a PreRecieveError' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_create_branch)
+ .and_raise(custom_hook_error)
+
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(Gitlab::Git::PreReceiveError)
+ expect(error.message).to eq(expected_message)
+ expect(error.raw_message).to eq(expected_raw_message)
+ end
+ end
+ end
+
+ context 'when details contain stderr without prefix' do
+ let(:stderr) { "something" }
+ let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" }
+ let(:expected_message) { error_message }
+ let(:expected_raw_message) { stderr }
+
+ it_behaves_like 'failed branch creation'
+ end
+
+ context 'when details contain stderr with prefix' do
+ let(:stderr) { "GL-HOOK-ERR: something" }
+ let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" }
+ let(:expected_message) { "something" }
+ let(:expected_raw_message) { stderr }
+
+ it_behaves_like 'failed branch creation'
+ end
+
+ context 'when details contain stdout without prefix' do
+ let(:stderr) { " \n" }
+ let(:stdout) { "something" }
+ let(:expected_message) { error_message }
+ let(:expected_raw_message) { stdout }
+
+ it_behaves_like 'failed branch creation'
+ end
+
+ context 'when details contain stdout with prefix' do
+ let(:stderr) { " \n" }
+ let(:stdout) { "GL-HOOK-ERR: something" }
+ let(:expected_message) { "something" }
+ let(:expected_raw_message) { stdout }
+
+ it_behaves_like 'failed branch creation'
+ end
+
+ context 'when details contain no stderr or stdout' do
+ let(:stderr) { " \n" }
+ let(:stdout) { "\n \n" }
+ let(:expected_message) { error_message }
+ let(:expected_raw_message) { "\n \n" }
+
+ it_behaves_like 'failed branch creation'
+ end
+ end
+ end
end
describe '#user_update_branch' do
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 277276bb1d3..b7c21516c77 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -156,35 +156,84 @@ RSpec.describe Gitlab::GitalyClient::RefService do
end
describe '#local_branches' do
- it 'sends a find_local_branches message' do
- expect_any_instance_of(Gitaly::RefService::Stub)
- .to receive(:find_local_branches)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return([])
+ let(:remote_name) { 'my_remote' }
- client.local_branches
- end
+ shared_examples 'common examples' do
+ it 'sends a find_local_branches message' do
+ target_commits = create_list(:gitaly_commit, 4)
+ branches = target_commits.each_with_index.map do |gitaly_commit, i|
+ Gitaly::FindLocalBranchResponse.new(
+ name: "#{remote_name}/#{i}",
+ commit: gitaly_commit,
+ commit_author: Gitaly::FindLocalBranchCommitAuthor.new(
+ name: gitaly_commit.author.name,
+ email: gitaly_commit.author.email,
+ date: gitaly_commit.author.date,
+ timezone: gitaly_commit.author.timezone
+ ),
+ commit_committer: Gitaly::FindLocalBranchCommitAuthor.new(
+ name: gitaly_commit.committer.name,
+ email: gitaly_commit.committer.email,
+ date: gitaly_commit.committer.date,
+ timezone: gitaly_commit.committer.timezone
+ )
+ )
+ end
+ local_branches = target_commits.each_with_index.map do |gitaly_commit, i|
+ Gitaly::Branch.new(name: "#{remote_name}/#{i}", target_commit: gitaly_commit)
+ end
+ response = [
+ Gitaly::FindLocalBranchesResponse.new(branches: branches[0, 2], local_branches: local_branches[0, 2]),
+ Gitaly::FindLocalBranchesResponse.new(branches: branches[2, 2], local_branches: local_branches[2, 2])
+ ]
- it 'parses and sends the sort parameter' do
- expect_any_instance_of(Gitaly::RefService::Stub)
- .to receive(:find_local_branches)
- .with(gitaly_request_with_params(sort_by: :UPDATED_DESC), kind_of(Hash))
- .and_return([])
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_local_branches)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response)
+
+ subject = client.local_branches
+
+ expect(subject.length).to be(target_commits.length)
+ end
+
+ it 'parses and sends the sort parameter' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_local_branches)
+ .with(gitaly_request_with_params(sort_by: :UPDATED_DESC), kind_of(Hash))
+ .and_return([])
+
+ client.local_branches(sort_by: 'updated_desc')
+ end
+
+ it 'translates known mismatches on sort param values' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_local_branches)
+ .with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash))
+ .and_return([])
+
+ client.local_branches(sort_by: 'name_asc')
+ end
- client.local_branches(sort_by: 'updated_desc')
+ it 'raises an argument error if an invalid sort_by parameter is passed' do
+ expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError)
+ end
end
- it 'translates known mismatches on sort param values' do
- expect_any_instance_of(Gitaly::RefService::Stub)
- .to receive(:find_local_branches)
- .with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash))
- .and_return([])
+ context 'when feature flag :gitaly_simplify_find_local_branches_response is enabled' do
+ before do
+ stub_feature_flags(gitaly_simplify_find_local_branches_response: true)
+ end
- client.local_branches(sort_by: 'name_asc')
+ it_behaves_like 'common examples'
end
- it 'raises an argument error if an invalid sort_by parameter is passed' do
- expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError)
+ context 'when feature flag :gitaly_simplify_find_local_branches_response is disabled' do
+ before do
+ stub_feature_flags(gitaly_simplify_find_local_branches_response: false)
+ end
+
+ it_behaves_like 'common examples'
end
end
@@ -211,6 +260,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do
client.tags(sort_by: 'name_asc')
end
+
+ context 'with semantic version sorting' do
+ it 'sends a correct find_all_tags message' do
+ expected_sort_by = Gitaly::FindAllTagsRequest::SortBy.new(
+ key: :VERSION_REFNAME,
+ direction: :ASCENDING
+ )
+
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_all_tags)
+ .with(gitaly_request_with_params(sort_by: expected_sort_by), kind_of(Hash))
+ .and_return([])
+
+ client.tags(sort_by: 'version_asc')
+ end
+ end
end
context 'with pagination option' do
diff --git a/spec/lib/gitlab/gitaly_client/server_service_spec.rb b/spec/lib/gitlab/gitaly_client/server_service_spec.rb
new file mode 100644
index 00000000000..615f2ce0c21
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/server_service_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GitalyClient::ServerService do
+ let(:storage) { 'default' }
+
+ describe '#readiness_check' do
+ before do
+ ::Gitlab::GitalyClient.clear_stubs!
+ end
+
+ let(:request) do
+ Gitaly::ReadinessCheckRequest.new(timeout: 30)
+ end
+
+ subject(:readiness_check) { described_class.new(storage).readiness_check }
+
+ it 'returns a positive success if no failures happened' do
+ expect_next_instance_of(Gitaly::ServerService::Stub) do |service|
+ response = Gitaly::ReadinessCheckResponse.new(ok_response: Gitaly::ReadinessCheckResponse::Ok.new)
+ expect(service).to receive(:readiness_check).with(request, kind_of(Hash)).and_return(response)
+ end
+
+ expect(readiness_check[:success]).to eq(true)
+ end
+
+ it 'returns a negative success and a compiled message if at least one failure happened' do
+ failure1 = Gitaly::ReadinessCheckResponse::Failure::Response.new(name: '1', error_message: 'msg 1')
+ failure2 = Gitaly::ReadinessCheckResponse::Failure::Response.new(name: '2', error_message: 'msg 2')
+ failures = Gitaly::ReadinessCheckResponse::Failure.new(failed_checks: [failure1, failure2])
+ response = Gitaly::ReadinessCheckResponse.new(failure_response: failures)
+
+ expect_next_instance_of(Gitaly::ServerService::Stub) do |service|
+ expect(service).to receive(:readiness_check).with(request, kind_of(Hash)).and_return(response)
+ end
+
+ expect(readiness_check[:success]).to eq(false)
+ expect(readiness_check[:message]).to eq("1: msg 1\n2: msg 2")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
index b6589a08f7d..ae7c3789051 100644
--- a/spec/lib/gitlab/gitaly_client/util_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GitalyClient::Util do
describe '.repository' do
diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
new file mode 100644
index 00000000000..57391e06192
--- /dev/null
+++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::AttachmentsDownloader do
+ subject(:downloader) { described_class.new(file_url) }
+
+ let_it_be(:file_url) { 'https://example.com/avatar.png' }
+ let_it_be(:content_type) { 'application/octet-stream' }
+
+ let(:content_length) { 1000 }
+ let(:chunk_double) { instance_double(HTTParty::FragmentWithResponse, code: 200) }
+ let(:headers_double) do
+ instance_double(
+ HTTParty::Response,
+ code: 200,
+ success?: true,
+ parsed_response: {},
+ headers: {
+ 'content-length' => content_length,
+ 'content-type' => content_type
+ }
+ )
+ end
+
+ describe '#perform' do
+ before do
+ allow(Gitlab::HTTP).to receive(:perform_request)
+ .with(Net::HTTP::Get, file_url, stream_body: true).and_yield(chunk_double)
+ allow(Gitlab::HTTP).to receive(:perform_request)
+ .with(Net::HTTP::Head, file_url, {}).and_return(headers_double)
+ end
+
+ context 'when file valid' do
+ it 'downloads file' do
+ file = downloader.perform
+
+ expect(File.exist?(file.path)).to eq(true)
+ end
+ end
+
+ context 'when filename is malicious' do
+ let_it_be(:file_url) { 'https://example.com/ava%2F..%2Ftar.png' }
+
+ it 'raises expected exception' do
+ expect { downloader.perform }.to raise_exception(
+ Gitlab::Utils::PathTraversalAttackError,
+ 'Invalid path'
+ )
+ end
+ end
+
+ context 'when file size exceeds limit' do
+ let(:content_length) { 26.megabytes }
+
+ it 'raises expected exception' do
+ expect { downloader.perform }.to raise_exception(
+ Gitlab::GithubImport::AttachmentsDownloader::DownloadError,
+ 'File size 26 MB exceeds limit of 25 MB'
+ )
+ end
+ end
+
+ context 'when file name length exceeds limit' do
+ before do
+ stub_const('BulkImports::FileDownloads::FilenameFetch::FILENAME_SIZE_LIMIT', 2)
+ end
+
+ it 'chops filename' do
+ file = downloader.perform
+
+ expect(File.exist?(file.path)).to eq(true)
+ expect(File.basename(file)).to eq('av.png')
+ end
+ end
+ end
+
+ describe '#delete' do
+ let(:tmp_dir_path) { File.join(Dir.tmpdir, 'github_attachments_test') }
+ let(:file) do
+ downloader.mkdir_p(tmp_dir_path)
+ file = File.open("#{tmp_dir_path}/test.txt", 'wb')
+ file.write('foo')
+ file.close
+ file
+ end
+
+ before do
+ allow(downloader).to receive(:filepath).and_return(file.path)
+ end
+
+ it 'removes file with parent folder' do
+ downloader.delete
+ expect(Dir.exist?(tmp_dir_path)).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 2bd3910ad87..c88bb6de859 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -40,6 +40,22 @@ RSpec.describe Gitlab::GithubImport::Client do
end
end
+ describe '#repos' do
+ it 'returns the user\'s repositories as a hash' do
+ client = described_class.new('foo')
+
+ stub_request(:get, 'https://api.github.com/rate_limit')
+ .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
+
+ stub_request(:get, 'https://api.github.com/user/repos?page=1&page_length=10&per_page=100')
+ .to_return(status: 200, body: [{ id: 1 }, { id: 2 }].to_json, headers: { 'Content-Type' => 'application/json' })
+
+ repos = client.repos({ page: 1, page_length: 10 })
+
+ expect(repos).to match_array([{ id: 1 }, { id: 2 }])
+ end
+ end
+
describe '#repository' do
it 'returns the details of a repository' do
client = described_class.new('foo')
@@ -49,6 +65,20 @@ RSpec.describe Gitlab::GithubImport::Client do
client.repository('foo/bar')
end
+
+ it 'returns repository data as a hash' do
+ client = described_class.new('foo')
+
+ stub_request(:get, 'https://api.github.com/rate_limit')
+ .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
+
+ stub_request(:get, 'https://api.github.com/repos/foo/bar')
+ .to_return(status: 200, body: { id: 1 }.to_json, headers: { 'Content-Type' => 'application/json' })
+
+ repository = client.repository('foo/bar')
+
+ expect(repository).to eq({ id: 1 })
+ end
end
describe '#pull_request' do
@@ -98,6 +128,30 @@ RSpec.describe Gitlab::GithubImport::Client do
end
end
+ describe '#branches' do
+ it 'returns the branches' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:branches, 'foo/bar')
+
+ client.branches('foo/bar')
+ end
+ end
+
+ describe '#branch_protection' do
+ it 'returns the protection details for the given branch' do
+ client = described_class.new('foo')
+
+ expect(client.octokit)
+ .to receive(:branch_protection).with('org/repo', 'bar')
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.branch_protection('org/repo', 'bar')
+ end
+ end
+
describe '#each_page' do
let(:client) { described_class.new('foo') }
let(:object1) { double(:object1) }
@@ -234,7 +288,7 @@ RSpec.describe Gitlab::GithubImport::Client do
expect(client).to receive(:requests_remaining?).twice.and_return(true)
expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
- expect(client.with_rate_limit(&block_to_rate_limit)).to be(true)
+ expect(client.with_rate_limit(&block_to_rate_limit)).to eq({})
end
it 'retries and does not succeed' do
@@ -255,7 +309,7 @@ RSpec.describe Gitlab::GithubImport::Client do
expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
- expect(client.with_rate_limit(&block_to_rate_limit)).to be(true)
+ expect(client.with_rate_limit(&block_to_rate_limit)).to eq({})
end
it 'retries and does not succeed' do
@@ -559,7 +613,7 @@ RSpec.describe Gitlab::GithubImport::Client do
expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
- expect(client.search_repos_by_name('test')).to be(true)
+ expect(client.search_repos_by_name('test')).to eq({})
end
it 'retries and does not succeed' do
@@ -599,7 +653,7 @@ RSpec.describe Gitlab::GithubImport::Client do
call_count = 0
allow(client.octokit).to receive(method) do
call_count += 1
- call_count > 1 ? true : raise(described_class::CLIENT_CONNECTION_ERROR, 'execution expired')
+ call_count > 1 ? {} : raise(described_class::CLIENT_CONNECTION_ERROR, 'execution expired')
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb
index 2f6f727dc38..dbc72574ec2 100644
--- a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb
@@ -6,31 +6,30 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:author) { create(:user) }
let_it_be(:assignee) { create(:user) }
- let_it_be(:assigner) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
- let(:issue) { create(:issue, project: project) }
+ let(:issuable) { create(:issue, project: project) }
let(:issue_event) do
Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
'id' => 6501124486,
- 'actor' => { 'id' => 4, 'login' => 'alice' },
+ 'actor' => { 'id' => author.id, 'login' => author.username },
'event' => event_type,
'commit_id' => nil,
'created_at' => '2022-04-26 18:30:53 UTC',
- 'assigner' => { 'id' => assigner.id, 'login' => assigner.username },
'assignee' => { 'id' => assignee.id, 'login' => assignee.username },
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
let(:note_attrs) do
{
- noteable_id: issue.id,
- noteable_type: Issue.name,
+ noteable_id: issuable.id,
+ noteable_type: issuable.class.name,
project_id: project.id,
- author_id: assigner.id,
+ author_id: author.id,
system: true,
created_at: issue_event.created_at,
updated_at: issue_event.created_at
@@ -45,12 +44,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do
}.stringify_keys
end
- shared_examples 'new note' do
+ shared_examples 'create expected notes' do
it 'creates expected note' do
- expect { importer.execute(issue_event) }.to change { issue.notes.count }
+ expect { importer.execute(issue_event) }.to change { issuable.notes.count }
.from(0).to(1)
- expect(issue.notes.last)
+ expect(issuable.notes.last)
.to have_attributes(expected_note_attrs)
end
@@ -67,29 +66,41 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do
end
end
+ shared_examples 'process assigned & unassigned events' do
+ context 'when importing an assigned event' do
+ let(:event_type) { 'assigned' }
+ let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") }
+
+ it_behaves_like 'create expected notes'
+ end
+
+ context 'when importing an unassigned event' do
+ let(:event_type) { 'unassigned' }
+ let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assignee.username}") }
+
+ it_behaves_like 'create expected notes'
+ end
+ end
+
describe '#execute' do
before do
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(issue.id)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
end
allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(author.id, author.username).and_return(author.id)
allow(finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id)
- allow(finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id)
end
end
- context 'when importing an assigned event' do
- let(:event_type) { 'assigned' }
- let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") }
-
- it_behaves_like 'new note'
+ context 'with Issue' do
+ it_behaves_like 'process assigned & unassigned events'
end
- context 'when importing an unassigned event' do
- let(:event_type) { 'unassigned' }
- let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assigner.username}") }
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
- it_behaves_like 'new note'
+ it_behaves_like 'process assigned & unassigned events'
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb
index e21672aa430..4476b4123ee 100644
--- a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
- let(:issue) { create(:issue, project: project) }
+ let(:issuable) { create(:issue, project: project) }
let!(:label) { create(:label, project: project) }
let(:issue_event) do
@@ -19,16 +19,14 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
'event' => event_type,
'commit_id' => nil,
'label_title' => label.title,
- 'issue_db_id' => issue.id,
'created_at' => '2022-04-26 18:30:53 UTC',
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
let(:event_attrs) do
{
user_id: user.id,
- issue_id: issue.id,
label_id: label.id,
created_at: issue_event.created_at
}.stringify_keys
@@ -36,9 +34,9 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
shared_examples 'new event' do
it 'creates a new label event' do
- expect { importer.execute(issue_event) }.to change { issue.resource_label_events.count }
+ expect { importer.execute(issue_event) }.to change { issuable.resource_label_events.count }
.from(0).to(1)
- expect(issue.resource_label_events.last)
+ expect(issuable.resource_label_events.last)
.to have_attributes(expected_event_attrs)
end
end
@@ -46,24 +44,44 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
before do
allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id)
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(issue.id)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
end
allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
end
- context 'when importing a labeled event' do
- let(:event_type) { 'labeled' }
- let(:expected_event_attrs) { event_attrs.merge(action: 'add') }
+ context 'with Issue' do
+ context 'when importing a labeled event' do
+ let(:event_type) { 'labeled' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') }
- it_behaves_like 'new event'
+ it_behaves_like 'new event'
+ end
+
+ context 'when importing an unlabeled event' do
+ let(:event_type) { 'unlabeled' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'remove') }
+
+ it_behaves_like 'new event'
+ end
end
- context 'when importing an unlabeled event' do
- let(:event_type) { 'unlabeled' }
- let(:expected_event_attrs) { event_attrs.merge(action: 'remove') }
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ context 'when importing a labeled event' do
+ let(:event_type) { 'labeled' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') }
- it_behaves_like 'new event'
+ it_behaves_like 'new event'
+ end
+
+ context 'when importing an unlabeled event' do
+ let(:event_type) { 'unlabeled' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'remove') }
+
+ it_behaves_like 'new event'
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb
index 2687627fc23..bc14b81bd91 100644
--- a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
- let(:issue) { create(:issue, project: project) }
+ let(:issuable) { create(:issue, project: project) }
let!(:milestone) { create(:milestone, project: project) }
let(:issue_event) do
@@ -19,16 +19,15 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do
'event' => event_type,
'commit_id' => nil,
'milestone_title' => milestone.title,
- 'issue_db_id' => issue.id,
+ 'issue_db_id' => issuable.id,
'created_at' => '2022-04-26 18:30:53 UTC',
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
let(:event_attrs) do
{
user_id: user.id,
- issue_id: issue.id,
milestone_id: milestone.id,
state: 'opened',
created_at: issue_event.created_at
@@ -37,9 +36,9 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do
shared_examples 'new event' do
it 'creates a new milestone event' do
- expect { importer.execute(issue_event) }.to change { issue.resource_milestone_events.count }
+ expect { importer.execute(issue_event) }.to change { issuable.resource_milestone_events.count }
.from(0).to(1)
- expect(issue.resource_milestone_events.last)
+ expect(issuable.resource_milestone_events.last)
.to have_attributes(expected_event_attrs)
end
end
@@ -48,25 +47,45 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do
before do
allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(milestone.id)
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(issue.id)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
end
allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
end
- context 'when importing a milestoned event' do
- let(:event_type) { 'milestoned' }
- let(:expected_event_attrs) { event_attrs.merge(action: 'add') }
+ context 'with Issue' do
+ context 'when importing a milestoned event' do
+ let(:event_type) { 'milestoned' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') }
- it_behaves_like 'new event'
+ it_behaves_like 'new event'
+ end
+
+ context 'when importing demilestoned event' do
+ let(:event_type) { 'demilestoned' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'remove') }
+
+ it_behaves_like 'new event'
+ end
end
- context 'when importing demilestoned event' do
- let(:event_type) { 'demilestoned' }
- let(:expected_event_attrs) { event_attrs.merge(action: 'remove') }
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ context 'when importing a milestoned event' do
+ let(:event_type) { 'milestoned' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') }
- it_behaves_like 'new event'
+ it_behaves_like 'new event'
+ end
+
+ context 'when importing demilestoned event' do
+ let(:event_type) { 'demilestoned' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'remove') }
+
+ it_behaves_like 'new event'
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb
new file mode 100644
index 00000000000..ff813dd41d9
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedReviewer do
+ subject(:importer) { described_class.new(project, client) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:requested_reviewer) { create(:user) }
+ let_it_be(:review_requester) { create(:user) }
+
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ let(:issue_event) do
+ Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
+ 'id' => 6501124486,
+ 'actor' => { 'id' => 4, 'login' => 'alice' },
+ 'event' => event_type,
+ 'commit_id' => nil,
+ 'created_at' => '2022-04-26 18:30:53 UTC',
+ 'review_requester' => { 'id' => review_requester.id, 'login' => review_requester.username },
+ 'requested_reviewer' => { 'id' => requested_reviewer.id, 'login' => requested_reviewer.username },
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
+ )
+ end
+
+ let(:note_attrs) do
+ {
+ noteable_id: issuable.id,
+ noteable_type: issuable.class.name,
+ project_id: project.id,
+ author_id: review_requester.id,
+ system: true,
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ }.stringify_keys
+ end
+
+ let(:expected_system_note_metadata_attrs) do
+ {
+ action: 'reviewer',
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ }.stringify_keys
+ end
+
+ shared_examples 'create expected notes' do
+ it 'creates expected note' do
+ expect { importer.execute(issue_event) }.to change { issuable.notes.count }
+ .from(0).to(1)
+
+ expect(issuable.notes.last)
+ .to have_attributes(expected_note_attrs)
+ end
+
+ it 'creates expected system note metadata' do
+ expect { importer.execute(issue_event) }.to change(SystemNoteMetadata, :count)
+ .from(0).to(1)
+
+ expect(SystemNoteMetadata.last)
+ .to have_attributes(
+ expected_system_note_metadata_attrs.merge(
+ note_id: Note.last.id
+ )
+ )
+ end
+ end
+
+ shared_examples 'process review_requested & review_request_removed MR events' do
+ context 'when importing a review_requested event' do
+ let(:event_type) { 'review_requested' }
+ let(:expected_note_attrs) { note_attrs.merge(note: "requested review from @#{requested_reviewer.username}") }
+
+ it_behaves_like 'create expected notes'
+ end
+
+ context 'when importing a review_request_removed event' do
+ let(:event_type) { 'review_request_removed' }
+ let(:expected_note_attrs) { note_attrs.merge(note: "removed review request for @#{requested_reviewer.username}") }
+
+ it_behaves_like 'create expected notes'
+ end
+ end
+
+ describe '#execute' do
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
+ allow(finder).to receive(:database_id).and_return(issuable.id)
+ end
+ allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(requested_reviewer.id, requested_reviewer.username)
+ .and_return(requested_reviewer.id)
+ allow(finder).to receive(:find).with(review_requester.id, review_requester.username)
+ .and_return(review_requester.id)
+ end
+ end
+
+ it_behaves_like 'process review_requested & review_request_removed MR events'
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb
index 9a49d80a8bb..f7e38f373c0 100644
--- a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
- let(:issue) { create(:issue, project: project) }
+ let(:issuable) { create(:issue, project: project) }
let(:commit_id) { nil }
let(:issue_event) do
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do
'event' => 'closed',
'created_at' => '2022-04-26 18:30:53 UTC',
'commit_id' => commit_id,
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
@@ -29,54 +29,74 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do
{
project_id: project.id,
author_id: user.id,
- target_id: issue.id,
- target_type: Issue.name,
+ target_id: issuable.id,
+ target_type: issuable.class.name,
action: 'closed',
created_at: issue_event.created_at,
updated_at: issue_event.created_at
}.stringify_keys
end
- let(:expected_state_event_attrs) do
- {
- user_id: user.id,
- issue_id: issue.id,
- state: 'closed',
- created_at: issue_event.created_at
- }.stringify_keys
- end
-
before do
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(issue.id)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
end
allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
end
- it 'creates expected event and state event' do
- importer.execute(issue_event)
+ shared_examples 'new event' do
+ it 'creates expected event and state event' do
+ importer.execute(issue_event)
+
+ expect(issuable.events.count).to eq 1
+ expect(issuable.events[0].attributes)
+ .to include expected_event_attrs
+
+ expect(issuable.resource_state_events.count).to eq 1
+ expect(issuable.resource_state_events[0].attributes)
+ .to include expected_state_event_attrs
+ end
+
+ context 'when closed by commit' do
+ let!(:closing_commit) { create(:commit, project: project) }
+ let(:commit_id) { closing_commit.id }
- expect(issue.events.count).to eq 1
- expect(issue.events[0].attributes)
- .to include expected_event_attrs
+ it 'creates expected event and state event' do
+ importer.execute(issue_event)
- expect(issue.resource_state_events.count).to eq 1
- expect(issue.resource_state_events[0].attributes)
- .to include expected_state_event_attrs
+ expect(issuable.events.count).to eq 1
+ state_event = issuable.resource_state_events.last
+ expect(state_event.source_commit).to eq commit_id[0..40]
+ end
+ end
end
- context 'when closed by commit' do
- let!(:closing_commit) { create(:commit, project: project) }
- let(:commit_id) { closing_commit.id }
+ context 'with Issue' do
+ let(:expected_state_event_attrs) do
+ {
+ user_id: user.id,
+ issue_id: issuable.id,
+ state: 'closed',
+ created_at: issue_event.created_at
+ }.stringify_keys
+ end
- it 'creates expected event and state event' do
- importer.execute(issue_event)
+ it_behaves_like 'new event'
+ end
- expect(issue.events.count).to eq 1
- state_event = issue.resource_state_events.last
- expect(state_event.source_commit).to eq commit_id[0..40]
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+ let(:expected_state_event_attrs) do
+ {
+ user_id: user.id,
+ merge_request_id: issuable.id,
+ state: 'closed',
+ created_at: issue_event.created_at
+ }.stringify_keys
end
+
+ it_behaves_like 'new event'
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb
index 68e001c7364..bf19147d4c8 100644
--- a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb
@@ -9,9 +9,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
-
let(:issue_iid) { 999 }
- let(:issue) { create(:issue, project: project, iid: issue_iid) }
+ let(:issuable) { create(:issue, project: project, iid: issue_iid) }
let(:referenced_in) { build_stubbed(:issue, project: project, iid: issue_iid + 1) }
let(:commit_id) { nil }
@@ -30,7 +29,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g
}
},
'created_at' => '2022-04-26 18:30:53 UTC',
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
@@ -38,8 +37,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g
let(:expected_note_attrs) do
{
system: true,
- noteable_type: Issue.name,
- noteable_id: issue.id,
+ noteable_type: issuable.class.name,
+ noteable_id: issuable.id,
project_id: project.id,
author_id: user.id,
note: expected_note_body,
@@ -47,58 +46,70 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g
}.stringify_keys
end
- context 'when referenced in other issue' do
- let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" }
-
- before do
- allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(referenced_in.iid)
- allow(finder).to receive(:database_id).and_return(issue.id)
- end
- allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
- allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
+ shared_examples 'import cross-referenced event' do
+ context 'when referenced in other issue' do
+ let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" }
+
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
+ allow(finder).to receive(:database_id).and_return(referenced_in.iid)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
+ end
+ allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
+ end
end
- end
- it 'creates expected note' do
- importer.execute(issue_event)
+ it 'creates expected note' do
+ importer.execute(issue_event)
- expect(issue.notes.count).to eq 1
- expect(issue.notes[0]).to have_attributes expected_note_attrs
- expect(issue.notes[0].system_note_metadata.action).to eq 'cross_reference'
+ expect(issuable.notes.count).to eq 1
+ expect(issuable.notes[0]).to have_attributes expected_note_attrs
+ expect(issuable.notes[0].system_note_metadata.action).to eq 'cross_reference'
+ end
end
- end
- context 'when referenced in pull request' do
- let(:referenced_in) { build_stubbed(:merge_request, project: project) }
- let(:pull_request_resource) { { 'id' => referenced_in.iid } }
+ context 'when referenced in pull request' do
+ let(:referenced_in) { build_stubbed(:merge_request, project: project) }
+ let(:pull_request_resource) { { 'id' => referenced_in.iid } }
- let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" }
+ let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" }
- before do
- allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(referenced_in.iid)
- allow(finder).to receive(:database_id).and_return(issue.id)
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
+ allow(finder).to receive(:database_id).and_return(referenced_in.iid)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
+ end
+ allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
+ end
end
- allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
- allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
+
+ it 'creates expected note' do
+ importer.execute(issue_event)
+
+ expect(issuable.notes.count).to eq 1
+ expect(issuable.notes[0]).to have_attributes expected_note_attrs
+ expect(issuable.notes[0].system_note_metadata.action).to eq 'cross_reference'
end
end
- it 'creates expected note' do
- importer.execute(issue_event)
+ context 'when referenced in out of project issue/pull_request' do
+ it 'does not create expected note' do
+ importer.execute(issue_event)
- expect(issue.notes.count).to eq 1
- expect(issue.notes[0]).to have_attributes expected_note_attrs
- expect(issue.notes[0].system_note_metadata.action).to eq 'cross_reference'
+ expect(issuable.notes.count).to eq 0
+ end
end
end
- context 'when referenced in out of project issue/pull_request' do
- it 'does not create expected note' do
- importer.execute(issue_event)
+ context 'with Issue' do
+ it_behaves_like 'import cross-referenced event'
+ end
- expect(issue.notes.count).to eq 0
- end
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ it_behaves_like 'import cross-referenced event'
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb
index 316ea798965..29598cb4354 100644
--- a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
+ let(:issuable) { create(:issue, project: project) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:issue_event) do
@@ -20,14 +20,14 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do
'created_at' => '2022-04-26 18:30:53 UTC',
'old_title' => 'old title',
'new_title' => 'new title',
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
let(:expected_note_attrs) do
{
- noteable_id: issue.id,
- noteable_type: Issue.name,
+ noteable_id: issuable.id,
+ noteable_type: issuable.class.name,
project_id: project.id,
author_id: user.id,
note: "changed title from **{-old-} title** to **{+new+} title**",
@@ -48,31 +48,43 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do
describe '#execute' do
before do
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(issue.id)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
end
allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
end
- it 'creates expected note' do
- expect { importer.execute(issue_event) }.to change { issue.notes.count }
- .from(0).to(1)
+ shared_examples 'import renamed event' do
+ it 'creates expected note' do
+ expect { importer.execute(issue_event) }.to change { issuable.notes.count }
+ .from(0).to(1)
- expect(issue.notes.last)
- .to have_attributes(expected_note_attrs)
- end
+ expect(issuable.notes.last)
+ .to have_attributes(expected_note_attrs)
+ end
- it 'creates expected system note metadata' do
- expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count }
- .from(0).to(1)
+ it 'creates expected system note metadata' do
+ expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count }
+ .from(0).to(1)
- expect(SystemNoteMetadata.last)
- .to have_attributes(
- expected_system_note_metadata_attrs.merge(
- note_id: Note.last.id
+ expect(SystemNoteMetadata.last)
+ .to have_attributes(
+ expected_system_note_metadata_attrs.merge(
+ note_id: Note.last.id
+ )
)
- )
+ end
+ end
+
+ context 'with Issue' do
+ it_behaves_like 'import renamed event'
+ end
+
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ it_behaves_like 'import renamed event'
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb
index 2461dbb9701..354003fc997 100644
--- a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
- let(:issue) { create(:issue, project: project) }
+ let(:issuable) { create(:issue, project: project) }
let(:issue_event) do
Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail
'actor' => { 'id' => user.id, 'login' => user.username },
'event' => 'reopened',
'created_at' => '2022-04-26 18:30:53 UTC',
- 'issue' => { 'number' => issue.iid }
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
end
@@ -27,40 +27,61 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail
{
project_id: project.id,
author_id: user.id,
- target_id: issue.id,
- target_type: Issue.name,
+ target_id: issuable.id,
+ target_type: issuable.class.name,
action: 'reopened',
created_at: issue_event.created_at,
updated_at: issue_event.created_at
}.stringify_keys
end
- let(:expected_state_event_attrs) do
- {
- user_id: user.id,
- state: 'reopened',
- created_at: issue_event.created_at
- }.stringify_keys
- end
-
before do
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
- allow(finder).to receive(:database_id).and_return(issue.id)
+ allow(finder).to receive(:database_id).and_return(issuable.id)
end
allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
end
- it 'creates expected event and state event' do
- importer.execute(issue_event)
+ shared_examples 'new event' do
+ it 'creates expected event and state event' do
+ importer.execute(issue_event)
- expect(issue.events.count).to eq 1
- expect(issue.events[0].attributes)
- .to include expected_event_attrs
+ expect(issuable.events.count).to eq 1
+ expect(issuable.events[0].attributes)
+ .to include expected_event_attrs
+
+ expect(issuable.resource_state_events.count).to eq 1
+ expect(issuable.resource_state_events[0].attributes)
+ .to include expected_state_event_attrs
+ end
+ end
+
+ context 'with Issue' do
+ let(:expected_state_event_attrs) do
+ {
+ user_id: user.id,
+ issue_id: issuable.id,
+ state: 'reopened',
+ created_at: issue_event.created_at
+ }.stringify_keys
+ end
+
+ it_behaves_like 'new event'
+ end
+
+ context 'with MergeRequest' do
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+ let(:expected_state_event_attrs) do
+ {
+ user_id: user.id,
+ merge_request_id: issuable.id,
+ state: 'reopened',
+ created_at: issue_event.created_at
+ }.stringify_keys
+ end
- expect(issue.resource_state_events.count).to eq 1
- expect(issue.resource_state_events[0].attributes)
- .to include expected_state_event_attrs
+ it_behaves_like 'new event'
end
end
diff --git a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
index 49a76fb5e6b..d28640a4f07 100644
--- a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter do
describe '#execute' do
diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb
index 33d5fbf13a0..91121f3c3fc 100644
--- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb
@@ -42,10 +42,6 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab
end
describe '#execute' do
- before do
- issue_event.attributes[:issue_db_id] = issue.id
- end
-
context "when it's closed issue event" do
let(:event_name) { 'closed' }
@@ -116,6 +112,20 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab
Gitlab::GithubImport::Importer::Events::ChangedAssignee
end
+ context "when it's review_requested issue event" do
+ let(:event_name) { 'review_requested' }
+
+ it_behaves_like 'triggers specific event importer',
+ Gitlab::GithubImport::Importer::Events::ChangedReviewer
+ end
+
+ context "when it's review_request_removed issue event" do
+ let(:event_name) { 'review_request_removed' }
+
+ it_behaves_like 'triggers specific event importer',
+ Gitlab::GithubImport::Importer::Events::ChangedReviewer
+ end
+
context "when it's unknown issue event" do
let(:event_name) { 'fake' }
diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
index 8d4c1b01e50..2c1af4f8948 100644
--- a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do
let(:parallel) { true }
let(:issue_event) do
struct = Struct.new(
- :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone,
- :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app,
+ :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, :source,
+ :assignee, :assigner, :review_requester, :requested_reviewer, :issue, :created_at, :performed_via_github_app,
keyword_init: true
)
struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC')
diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
new file mode 100644
index 00000000000..6dc6db739f4
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
+ subject(:importer) { described_class.new(github_protected_branch, project, client) }
+
+ let(:allow_force_pushes_on_github) { true }
+ let(:github_protected_branch) do
+ Gitlab::GithubImport::Representation::ProtectedBranch.new(
+ id: 'protection',
+ allow_force_pushes: allow_force_pushes_on_github
+ )
+ end
+
+ let(:project) { create(:project, :repository) }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+
+ describe '#execute' do
+ let(:create_service) { instance_double('ProtectedBranches::CreateService') }
+
+ shared_examples 'create branch protection by the strictest ruleset' do
+ let(:expected_ruleset) do
+ {
+ name: 'protection',
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ allow_force_push: expected_allow_force_push
+ }
+ end
+
+ it 'calls service with the correct arguments' do
+ expect(ProtectedBranches::CreateService).to receive(:new).with(
+ project,
+ project.creator,
+ expected_ruleset
+ ).and_return(create_service)
+
+ expect(create_service).to receive(:execute).with(skip_authorization: true)
+ importer.execute
+ end
+
+ it 'creates protected branch and access levels for given github rule' do
+ expect { importer.execute }.to change(ProtectedBranch, :count).by(1)
+ .and change(ProtectedBranch::PushAccessLevel, :count).by(1)
+ .and change(ProtectedBranch::MergeAccessLevel, :count).by(1)
+ end
+ end
+
+ context 'when branch is protected on GitLab' do
+ before do
+ create(
+ :protected_branch,
+ project: project,
+ name: 'protect*',
+ allow_force_push: allow_force_pushes_on_gitlab
+ )
+ end
+
+ context 'when branch protection rule on Gitlab is stricter than on Github' do
+ let(:allow_force_pushes_on_github) { true }
+ let(:allow_force_pushes_on_gitlab) { false }
+ let(:expected_allow_force_push) { false }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+
+ context 'when branch protection rule on Github is stricter than on Gitlab' do
+ let(:allow_force_pushes_on_github) { false }
+ let(:allow_force_pushes_on_gitlab) { true }
+ let(:expected_allow_force_push) { false }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+
+ context 'when branch protection rules on Github and Gitlab are the same' do
+ let(:allow_force_pushes_on_github) { true }
+ let(:allow_force_pushes_on_gitlab) { true }
+ let(:expected_allow_force_push) { true }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+ end
+
+ context 'when branch is not protected on GitLab' do
+ let(:expected_allow_force_push) { true }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
new file mode 100644
index 00000000000..4e9208be985
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
+ subject(:importer) { described_class.new(project, client, parallel: parallel) }
+
+ let(:project) { instance_double('Project', id: 4, import_source: 'foo/bar') }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:parallel) { true }
+
+ let(:branches) do
+ branch = Struct.new(:name, :protection, keyword_init: true)
+ protection = Struct.new(:enabled, keyword_init: true)
+
+ [
+ branch.new(name: 'main', protection: protection.new(enabled: false)),
+ branch.new(name: 'staging', protection: protection.new(enabled: true)),
+ branch.new(name: 'development', protection: nil) # when user has no admin right for this repo
+ ]
+ end
+
+ let(:github_protection_rule) do
+ response = Struct.new(:name, :url, :required_signatures, :enforce_admins, :required_linear_history,
+ :allow_force_pushes, :allow_deletion, :block_creations, :required_conversation_resolution,
+ keyword_init: true
+ )
+ required_signatures = Struct.new(:url, :enabled, keyword_init: true)
+ enforce_admins = Struct.new(:url, :enabled, keyword_init: true)
+ allow_option = Struct.new(:enabled, keyword_init: true)
+ response.new(
+ name: 'main',
+ url: 'https://example.com/branches/main/protection',
+ required_signatures: required_signatures.new(
+ url: 'https://example.com/branches/main/protection/required_signatures',
+ enabled: false
+ ),
+ enforce_admins: enforce_admins.new(
+ url: 'https://example.com/branches/main/protection/enforce_admins',
+ enabled: false
+ ),
+ required_linear_history: allow_option.new(
+ enabled: false
+ ),
+ allow_force_pushes: allow_option.new(
+ enabled: false
+ ),
+ allow_deletion: allow_option.new(
+ enabled: false
+ ),
+ block_creations: allow_option.new(
+ enabled: true
+ ),
+ required_conversation_resolution: allow_option.new(
+ enabled: false
+ )
+ )
+ end
+
+ describe '#parallel?' do
+ context 'when running in parallel mode' do
+ it { expect(importer).to be_parallel }
+ end
+
+ context 'when running in sequential mode' do
+ let(:parallel) { false }
+
+ it { expect(importer).not_to be_parallel }
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports protected branches in parallel' do
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ let(:parallel) { false }
+
+ it 'imports protected branches in sequence' do
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import', :clean_gitlab_redis_cache do
+ let(:parallel) { false }
+
+ before do
+ allow(client).to receive(:branches).and_return(branches)
+ allow(client)
+ .to receive(:branch_protection)
+ .with(project.import_source, 'staging')
+ .and_return(github_protection_rule)
+ .once
+ end
+
+ it 'imports each protected branch in sequence' do
+ protected_branch_importer = instance_double('Gitlab::GithubImport::Importer::ProtectedBranchImporter')
+
+ expect(Gitlab::GithubImport::Importer::ProtectedBranchImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::ProtectedBranch),
+ project,
+ client
+ )
+ .and_return(protected_branch_importer)
+
+ expect(protected_branch_importer).to receive(:execute)
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment).with(project, :protected_branch, :fetched)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import', :clean_gitlab_redis_cache do
+ before do
+ allow(client).to receive(:branches).and_return(branches)
+ allow(client)
+ .to receive(:branch_protection)
+ .with(project.import_source, 'staging')
+ .and_return(github_protection_rule)
+ .once
+ end
+
+ it 'imports each protected branch in parallel' do
+ expect(Gitlab::GithubImport::ImportProtectedBranchWorker)
+ .to receive(:bulk_perform_in)
+ .with(
+ 1.second,
+ [[project.id, an_instance_of(Hash), an_instance_of(String)]],
+ batch_delay: 1.minute,
+ batch_size: 1000
+ )
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment).with(project, :protected_branch, :fetched)
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#each_object_to_import', :clean_gitlab_redis_cache do
+ let(:branch_struct) { Struct.new(:protection, :name, :url, keyword_init: true) }
+ let(:protection_struct) { Struct.new(:enabled, keyword_init: true) }
+ let(:protected_branch) { branch_struct.new(name: 'main', protection: protection_struct.new(enabled: true)) }
+ let(:unprotected_branch) { branch_struct.new(name: 'staging', protection: protection_struct.new(enabled: false)) }
+ # when user has no admin rights on repo
+ let(:unknown_protection_branch) { branch_struct.new(name: 'development', protection: nil) }
+
+ let(:page_counter) { instance_double(Gitlab::GithubImport::PageCounter) }
+
+ before do
+ allow(client).to receive(:branches).with(project.import_source)
+ .and_return([protected_branch, unprotected_branch, unknown_protection_branch])
+ allow(client).to receive(:branch_protection)
+ .with(project.import_source, protected_branch.name).once
+ .and_return(github_protection_rule)
+ allow(Gitlab::GithubImport::ObjectCounter).to receive(:increment)
+ .with(project, :protected_branch, :fetched)
+ end
+
+ it 'imports each protected branch page by page' do
+ subject.each_object_to_import do |object|
+ expect(object).to eq github_protection_rule
+ end
+ expect(Gitlab::GithubImport::ObjectCounter).to have_received(:increment).once
+ end
+
+ context 'when protected branch is already processed' do
+ it "doesn't process this branch" do
+ subject.mark_as_imported(protected_branch)
+
+ subject.each_object_to_import {}
+ expect(Gitlab::GithubImport::ObjectCounter).not_to have_received(:increment)
+ end
+ end
+ end
+
+ describe '#importer_class' do
+ it { expect(importer.importer_class).to eq Gitlab::GithubImport::Importer::ProtectedBranchImporter }
+ end
+
+ describe '#representation_class' do
+ it { expect(importer.representation_class).to eq Gitlab::GithubImport::Representation::ProtectedBranch }
+ end
+
+ describe '#sidekiq_worker_class' do
+ it { expect(importer.sidekiq_worker_class).to eq Gitlab::GithubImport::ImportProtectedBranchWorker }
+ end
+
+ describe '#object_type' do
+ it { expect(importer.object_type).to eq :protected_branch }
+ end
+
+ describe '#collection_method' do
+ it { expect(importer.collection_method).to eq :protected_branches }
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the ID of the given protected branch' do
+ expect(importer.id_for_already_imported_cache(github_protection_rule)).to eq('main')
+ end
+ end
+
+ describe '#collection_options' do
+ it 'returns an empty Hash' do
+ # For large projects (e.g. kubernetes/kubernetes) GitHub's API may produce
+ # HTTP 500 errors when using explicit sorting options, regardless of what
+ # order you sort in. Not using any sorting options at all allows us to
+ # work around this.
+ expect(importer.collection_options).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb
new file mode 100644
index 00000000000..4779f9c8982
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/release_attachments_importer_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter do
+ subject(:importer) { described_class.new(release_attachments, project, client) }
+
+ let_it_be(:project) { create(:project) }
+
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:release) { create(:release, project: project, description: description) }
+ let(:release_attachments) do
+ Gitlab::GithubImport::Representation::ReleaseAttachments
+ .from_json_hash(release_db_id: release.id, description: release.description)
+ end
+
+ let(:doc_url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' }
+ let(:image_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' }
+ let(:description) do
+ <<-TEXT.strip
+ Some text...
+
+ [special-doc](#{doc_url})
+ ![image.jpeg](#{image_url})
+ TEXT
+ end
+
+ describe '#execute' do
+ let(:downloader_stub) { instance_double(Gitlab::GithubImport::AttachmentsDownloader) }
+ let(:tmp_stub_doc) { Tempfile.create('attachment_download_test.txt') }
+ let(:tmp_stub_image) { Tempfile.create('image.jpeg') }
+
+ context 'when importing doc attachment' do
+ before do
+ allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(doc_url)
+ .and_return(downloader_stub)
+ allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(image_url)
+ .and_return(downloader_stub)
+ allow(downloader_stub).to receive(:perform).and_return(tmp_stub_doc, tmp_stub_image)
+ allow(downloader_stub).to receive(:delete).twice
+
+ allow(UploadService).to receive(:new)
+ .with(project, tmp_stub_doc, FileUploader).and_call_original
+ allow(UploadService).to receive(:new)
+ .with(project, tmp_stub_image, FileUploader).and_call_original
+ end
+
+ it 'updates release description with new attachment url' do
+ importer.execute
+
+ release.reload
+ expect(release.description).to start_with("Some text...\n\n [special-doc](/uploads/")
+ expect(release.description).to include('![image.jpeg](/uploads/')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb
new file mode 100644
index 00000000000..1aeb3462cd5
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/releases_attachments_importer_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter do
+ subject { described_class.new(project, client) }
+
+ let_it_be(:project) { create(:project) }
+
+ let(:client) { instance_double(Gitlab::GithubImport::Client) }
+
+ describe '#each_object_to_import', :clean_gitlab_redis_cache do
+ let!(:release_1) { create(:release, project: project) }
+ let!(:release_2) { create(:release, project: project) }
+
+ it 'iterates each project release' do
+ list = []
+ subject.each_object_to_import do |object|
+ list << object
+ end
+ expect(list).to contain_exactly(release_1, release_2)
+ end
+
+ context 'when release is already processed' do
+ it "doesn't process this release" do
+ subject.mark_as_imported(release_1)
+
+ list = []
+ subject.each_object_to_import do |object|
+ list << object
+ end
+ expect(list).to contain_exactly(release_2)
+ end
+ end
+ end
+
+ describe '#representation_class' do
+ it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::ReleaseAttachments) }
+ end
+
+ describe '#importer_class' do
+ it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter) }
+ end
+
+ describe '#sidekiq_worker_class' do
+ it { expect(subject.sidekiq_worker_class).to eq(Gitlab::GithubImport::ImportReleaseAttachmentsWorker) }
+ end
+
+ describe '#collection_method' do
+ it { expect(subject.collection_method).to eq(:release_attachments) }
+ end
+
+ describe '#object_type' do
+ it { expect(subject.object_type).to eq(:release_attachment) }
+ end
+
+ describe '#id_for_already_imported_cache' do
+ let(:release) { build_stubbed(:release) }
+
+ it { expect(subject.id_for_already_imported_cache(release)).to eq(release.id) }
+ end
+
+ describe '#object_representation' do
+ let(:release) { build_stubbed(:release) }
+
+ it 'returns release attachments representation' do
+ representation = subject.object_representation(release)
+
+ expect(representation.class).to eq subject.representation_class
+ expect(representation.release_db_id).to eq release.id
+ expect(representation.description).to eq release.description
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index f2730ba74ec..0b8b1922d94 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
describe '#import_wiki?' do
it 'returns true if the wiki should be imported' do
- repo = double(:repo, has_wiki: true)
+ repo = { has_wiki: true }
expect(client)
.to receive(:repository)
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'returns false if the GitHub wiki is disabled' do
- repo = double(:repo, has_wiki: false)
+ repo = { has_wiki: false }
expect(client)
.to receive(:repository)
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'returns false if the wiki has already been imported' do
- repo = double(:repo, has_wiki: true)
+ repo = { has_wiki: true }
expect(client)
.to receive(:repository)
@@ -186,7 +186,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
describe '#import_repository' do
it 'imports the repository' do
- repo = double(:repo, default_branch: 'develop')
+ repo = { default_branch: 'develop' }
expect(client)
.to receive(:repository)
diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb
index bb1ee79ad93..4ed01fd7e0b 100644
--- a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
let(:client) { double }
let_it_be(:project) { create(:project, :import_started, import_source: 'http://somegithub.com') }
- let_it_be(:issue) { create(:issue, project: project) }
+
+ let!(:issuable) { create(:issue, project: project) }
subject { described_class.new(project, client, parallel: parallel) }
@@ -35,7 +36,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
end
describe '#page_counter_id' do
- it { expect(subject.page_counter_id(issue)).to eq("issues/#{issue.iid}/issue_timeline") }
+ it { expect(subject.page_counter_id(issuable)).to eq("issues/#{issuable.iid}/issue_timeline") }
end
describe '#id_for_already_imported_cache' do
@@ -51,6 +52,39 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
end
end
+ describe '#compose_associated_id!' do
+ let(:issuable) { build_stubbed(:issue, iid: 99) }
+ let(:event_resource) { Struct.new(:id, :event, :source, keyword_init: true) }
+
+ context 'when event type is cross-referenced' do
+ let(:event) do
+ source_resource = Struct.new(:issue, keyword_init: true)
+ issue_resource = Struct.new(:id, keyword_init: true)
+ event_resource.new(
+ id: nil,
+ event: 'cross-referenced',
+ source: source_resource.new(issue: issue_resource.new(id: '100500'))
+ )
+ end
+
+ it 'assigns event id' do
+ subject.compose_associated_id!(issuable, event)
+
+ expect(event.id).to eq 'cross-reference#99-in-100500'
+ end
+ end
+
+ context "when event type isn't cross-referenced" do
+ let(:event) { event_resource.new(id: nil, event: 'labeled') }
+
+ it "doesn't assign event id" do
+ subject.compose_associated_id!(issuable, event)
+
+ expect(event.id).to eq nil
+ end
+ end
+ end
+
describe '#each_object_to_import', :clean_gitlab_redis_cache do
let(:issue_event) do
struct = Struct.new(:id, :event, :created_at, :issue, keyword_init: true)
@@ -72,19 +106,37 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
.with(
:issue_timeline,
project.import_source,
- issue.iid,
+ issuable.iid,
{ state: 'all', sort: 'created', direction: 'asc', page: 1 }
).and_yield(page)
end
- it 'imports each issue event page by page' do
- counter = 0
- subject.each_object_to_import do |object|
- expect(object).to eq issue_event
- expect(issue_event.issue['number']).to eq issue.iid
- counter += 1
+ context 'with issues' do
+ it 'imports each issue event page by page' do
+ counter = 0
+ subject.each_object_to_import do |object|
+ expect(object).to eq issue_event
+ expect(issue_event.issue['number']).to eq issuable.iid
+ expect(issue_event.issue['pull_request']).to eq false
+ counter += 1
+ end
+ expect(counter).to eq 1
+ end
+ end
+
+ context 'with merge requests' do
+ let!(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ it 'imports each merge request event page by page' do
+ counter = 0
+ subject.each_object_to_import do |object|
+ expect(object).to eq issue_event
+ expect(issue_event.issue['number']).to eq issuable.iid
+ expect(issue_event.issue['pull_request']).to eq true
+ counter += 1
+ end
+ expect(counter).to eq 1
end
- expect(counter).to eq 1
end
it 'triggers page number increment' do
@@ -103,7 +155,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
context 'when page is already processed' do
before do
page_counter = Gitlab::GithubImport::PageCounter.new(
- project, subject.page_counter_id(issue)
+ project, subject.page_counter_id(issuable)
)
page_counter.set(page.number)
end
diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb
index ad45469a4c3..1da6bb06403 100644
--- a/spec/lib/gitlab/github_import/markdown_text_spec.rb
+++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb
@@ -60,6 +60,34 @@ RSpec.describe Gitlab::GithubImport::MarkdownText do
end
end
+ describe '.fetch_attachment_urls' do
+ let(:image_extension) { described_class::MEDIA_TYPES.sample }
+ let(:image_attachment) do
+ "![special-image](https://user-images.githubusercontent.com/6833862/"\
+ "176685788-e7a93168-7ded-406a-82b5-eb1c56685a93.#{image_extension})"
+ end
+
+ let(:doc_extension) { described_class::DOC_TYPES.sample }
+ let(:doc_attachment) do
+ "[some-doc](https://github.com/nickname/public-test-repo/"\
+ "files/9020437/git-cheat-sheet.#{doc_extension})"
+ end
+
+ let(:text) do
+ <<-TEXT
+ Comment with an attachment
+ #{image_attachment}
+ #{FFaker::Lorem.sentence}
+ #{doc_attachment}
+ TEXT
+ end
+
+ it 'fetches attachment urls' do
+ expect(described_class.fetch_attachment_urls(text))
+ .to contain_exactly(image_attachment, doc_attachment)
+ end
+ end
+
describe '#to_s' do
it 'returns the text when the author was found' do
author = double(:author, login: 'Alice')
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index 738e7c88d7d..860bb60f3ed 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -15,6 +15,10 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
Class
end
+ def sidekiq_worker_class
+ Class
+ end
+
def object_type
:dummy
end
diff --git a/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb b/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb
index bcb8575bdbf..5a24f929388 100644
--- a/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormatter do
it 'does nothing when there is any text before the suggestion tag' do
diff --git a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
index d40be0e841c..43f0198704f 100644
--- a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
@@ -1,21 +1,41 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::ExposeAttribute do
- it 'defines a getter method that returns an attribute value' do
- klass = Class.new do
+ let(:klass) do
+ Class.new do
include Gitlab::GithubImport::Representation::ExposeAttribute
expose_attribute :number
attr_reader :attributes
- def initialize
- @attributes = { number: 42 }
+ def initialize(attributes)
+ @attributes = attributes
+ end
+ end
+ end
+
+ it 'defines a getter method that returns an attribute value' do
+ expect(klass.new({ number: 42 }).number).to eq(42)
+ end
+
+ describe '#[]' do
+ it 'returns exposed attributes value using array notation' do
+ expect(klass.new({ number: 42 })[:number]).to eq(42)
+ end
+
+ context 'when attribute does not exist' do
+ it 'returns nil' do
+ expect(klass.new({})[:number]).to eq(nil)
end
end
- expect(klass.new.number).to eq(42)
+ context 'when attribute is not exposed' do
+ it 'returns nil' do
+ expect(klass.new({ not_exposed_attribute: 42 })[:not_exposed_attribute]).to eq(nil)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb
index d3a98035e73..0256858ecf1 100644
--- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_actor) { false }
it 'does not return such info' do
- expect(issue_event.actor).to eq nil
+ expect(issue_event.actor).to be_nil
end
end
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_label) { false }
it 'does not return such info' do
- expect(issue_event.label_title).to eq nil
+ expect(issue_event.label_title).to be_nil
end
end
@@ -72,8 +72,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_rename) { false }
it 'does not return such info' do
- expect(issue_event.old_title).to eq nil
- expect(issue_event.new_title).to eq nil
+ expect(issue_event.old_title).to be_nil
+ expect(issue_event.new_title).to be_nil
end
end
@@ -87,30 +87,47 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_milestone) { false }
it 'does not return such info' do
- expect(issue_event.milestone_title).to eq nil
+ expect(issue_event.milestone_title).to be_nil
end
end
- context 'when assignee and assigner data is present' do
- it 'includes assignee and assigner details' do
+ context 'when assignee data is present' do
+ it 'includes assignee details' do
expect(issue_event.assignee)
.to be_an_instance_of(Gitlab::GithubImport::Representation::User)
expect(issue_event.assignee.id).to eq(5)
expect(issue_event.assignee.login).to eq('tom')
+ end
+ end
+
+ context 'when assignee data is empty' do
+ let(:with_assignee) { false }
- expect(issue_event.assigner)
+ it 'does not return such info' do
+ expect(issue_event.assignee).to be_nil
+ end
+ end
+
+ context 'when requested_reviewer and review_requester data is present' do
+ it 'includes requested_reviewer and review_requester details' do
+ expect(issue_event.requested_reviewer)
.to be_an_instance_of(Gitlab::GithubImport::Representation::User)
- expect(issue_event.assigner.id).to eq(6)
- expect(issue_event.assigner.login).to eq('jerry')
+ expect(issue_event.requested_reviewer.id).to eq(6)
+ expect(issue_event.requested_reviewer.login).to eq('mickey')
+
+ expect(issue_event.review_requester)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+ expect(issue_event.review_requester.id).to eq(7)
+ expect(issue_event.review_requester.login).to eq('minnie')
end
end
- context 'when assignee and assigner data is empty' do
- let(:with_assignee) { false }
+ context 'when requested_reviewer and review_requester data is empty' do
+ let(:with_reviewer) { false }
it 'does not return such info' do
- expect(issue_event.assignee).to eq nil
- expect(issue_event.assigner).to eq nil
+ expect(issue_event.requested_reviewer).to be_nil
+ expect(issue_event.review_requester).to be_nil
end
end
@@ -148,7 +165,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:response) do
event_resource = Struct.new(
:id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone,
- :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app,
+ :source, :assignee, :requested_reviewer, :review_requester, :issue, :created_at,
+ :performed_via_github_app,
keyword_init: true
)
user_resource = Struct.new(:id, :login, keyword_init: true)
@@ -166,7 +184,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
milestone: with_milestone ? { title: 'milestone title' } : nil,
source: { type: 'issue', id: 123456 },
assignee: with_assignee ? user_resource.new(id: 5, login: 'tom') : nil,
- assigner: with_assignee ? user_resource.new(id: 6, login: 'jerry') : nil,
+ requested_reviewer: with_reviewer ? user_resource.new(id: 6, login: 'mickey') : nil,
+ review_requester: with_reviewer ? user_resource.new(id: 7, login: 'minnie') : nil,
issue: { 'number' => 2, 'pull_request' => pull_request },
created_at: '2022-04-26 18:30:53 UTC',
performed_via_github_app: nil
@@ -178,6 +197,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_rename) { true }
let(:with_milestone) { true }
let(:with_assignee) { true }
+ let(:with_reviewer) { true }
let(:pull_request) { nil }
it_behaves_like 'an IssueEvent' do
@@ -203,7 +223,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
'milestone_title' => (with_milestone ? 'milestone title' : nil),
'source' => { 'type' => 'issue', 'id' => 123456 },
'assignee' => (with_assignee ? { 'id' => 5, 'login' => 'tom' } : nil),
- 'assigner' => (with_assignee ? { 'id' => 6, 'login' => 'jerry' } : nil),
+ 'requested_reviewer' => (with_reviewer ? { 'id' => 6, 'login' => 'mickey' } : nil),
+ 'review_requester' => (with_reviewer ? { 'id' => 7, 'login' => 'minnie' } : nil),
'issue' => { 'number' => 2, 'pull_request' => pull_request },
'created_at' => '2022-04-26 18:30:53 UTC',
'performed_via_github_app' => nil
@@ -215,6 +236,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_rename) { true }
let(:with_milestone) { true }
let(:with_assignee) { true }
+ let(:with_reviewer) { true }
let(:pull_request) { nil }
let(:issue_event) { described_class.from_json_hash(hash) }
diff --git a/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb b/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb
index b59ea513436..6663a7366a5 100644
--- a/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::LfsObject do
describe '#github_identifiers' do
diff --git a/spec/lib/gitlab/github_import/representation/note_spec.rb b/spec/lib/gitlab/github_import/representation/note_spec.rb
index 97addcc1c98..9f416eb3c02 100644
--- a/spec/lib/gitlab/github_import/representation/note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/note_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::Note do
let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
diff --git a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
new file mode 100644
index 00000000000..e762dc469c1
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do
+ shared_examples 'a ProtectedBranch rule' do
+ it 'returns an instance of ProtectedBranch' do
+ expect(protected_branch).to be_an_instance_of(described_class)
+ end
+
+ context 'with ProtectedBranch' do
+ it 'includes the protected branch ID (name)' do
+ expect(protected_branch.id).to eq 'main'
+ end
+
+ it 'includes the protected branch allow_force_pushes' do
+ expect(protected_branch.allow_force_pushes).to eq true
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ response = Struct.new(:url, :allow_force_pushes, keyword_init: true)
+ allow_force_pushes = Struct.new(:enabled, keyword_init: true)
+ response.new(
+ url: 'https://example.com/branches/main/protection',
+ allow_force_pushes: allow_force_pushes.new(
+ enabled: true
+ )
+ )
+ end
+
+ it_behaves_like 'a ProtectedBranch rule' do
+ let(:protected_branch) { described_class.from_api_response(response) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a ProtectedBranch rule' do
+ let(:hash) do
+ {
+ 'id' => 'main',
+ 'allow_force_pushes' => true
+ }
+ end
+
+ let(:protected_branch) { described_class.from_json_hash(hash) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
index f812fd85fbc..d6e7a8172f7 100644
--- a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc }
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
index 925dba5b5a7..deb9535a845 100644
--- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::PullRequest do
let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
diff --git a/spec/lib/gitlab/github_import/representation/release_attachments_spec.rb b/spec/lib/gitlab/github_import/representation/release_attachments_spec.rb
new file mode 100644
index 00000000000..0ef9dad6a13
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/release_attachments_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Representation::ReleaseAttachments do
+ shared_examples 'a Release attachments data' do
+ it 'returns an instance of ReleaseAttachments' do
+ expect(representation).to be_an_instance_of(described_class)
+ end
+
+ it 'includes release DB id' do
+ expect(representation.release_db_id).to eq 42
+ end
+
+ it 'includes release description' do
+ expect(representation.description).to eq 'Some text here..'
+ end
+ end
+
+ describe '.from_db_record' do
+ let(:release) { build_stubbed(:release, id: 42, description: 'Some text here..') }
+
+ it_behaves_like 'a Release attachments data' do
+ let(:representation) { described_class.from_db_record(release) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a Release attachments data' do
+ let(:hash) do
+ {
+ 'release_db_id' => 42,
+ 'description' => 'Some text here..'
+ }
+ end
+
+ let(:representation) { described_class.from_json_hash(hash) }
+ end
+ end
+
+ describe '#github_identifiers' do
+ it 'returns a hash with needed identifiers' do
+ release_id = rand(100)
+ representation = described_class.new(release_db_id: release_id, description: 'text')
+
+ expect(representation.github_identifiers).to eq({ db_id: release_id })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
index 2770e5c5397..739c832025c 100644
--- a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::ToHash do
describe '#to_hash' do
diff --git a/spec/lib/gitlab/github_import/representation/user_spec.rb b/spec/lib/gitlab/github_import/representation/user_spec.rb
index 14204886e9b..d7219556ada 100644
--- a/spec/lib/gitlab/github_import/representation/user_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/user_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::User do
shared_examples 'a User' do
diff --git a/spec/lib/gitlab/github_import/representation_spec.rb b/spec/lib/gitlab/github_import/representation_spec.rb
index 58c10c4a775..9a0ef45fc1d 100644
--- a/spec/lib/gitlab/github_import/representation_spec.rb
+++ b/spec/lib/gitlab/github_import/representation_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::GithubImport::Representation do
describe '.symbolize_hash' do
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index d85e298785c..8ebbff31f64 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -68,10 +68,16 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
it_behaves_like 'user ID finder', :assignee
end
- context 'when the author_key parameter is :assigner' do
- let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', assigner: user) }
+ context 'when the author_key parameter is :requested_reviewer' do
+ let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', requested_reviewer: user) }
- it_behaves_like 'user ID finder', :assigner
+ it_behaves_like 'user ID finder', :requested_reviewer
+ end
+
+ context 'when the author_key parameter is :review_requester' do
+ let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', review_requester: user) }
+
+ it_behaves_like 'user ID finder', :review_requester
end
end
end
diff --git a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb
index 487b19a98e0..5006d27c356 100644
--- a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb
+++ b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do
let(:log_entry) do
{
status: 200,
- time: {
+ time: {
total: 758.58,
db: 77.06,
view: 681.52
diff --git a/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
new file mode 100644
index 00000000000..5858986dfc8
--- /dev/null
+++ b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Graphql::Limit::FieldCallCount do
+ include GraphqlHelpers
+
+ let(:field_args) { {} }
+ let(:owner) { fresh_object_type }
+ let(:field) do
+ ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) do
+ extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
+ end
+ end
+
+ let(:query) do
+ GraphQL::Query.new(GitlabSchema)
+ end
+
+ def resolve_value
+ resolve_field(field, { value: 'foo' }, object_type: owner, query: query)
+ end
+
+ it 'allows the call' do
+ expect { resolve_value }.not_to raise_error
+ end
+
+ it 'executes the extension' do
+ expect(described_class).to receive(:new).and_call_original
+
+ resolve_value
+ end
+
+ it 'returns an error when the field is called multiple times' do
+ resolve_value
+
+ expect(resolve_value).to be_an_instance_of(Gitlab::Graphql::Errors::LimitError)
+ end
+
+ context 'when limit is not specified' do
+ let(:field) do
+ ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) do
+ extension(::Gitlab::Graphql::Limit::FieldCallCount)
+ end
+ end
+
+ it 'returns an error' do
+ expect(resolve_value).to be_an_instance_of(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
+ context 'when the field is not extended' do
+ let(:field) do
+ ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner)
+ end
+
+ it 'allows the call' do
+ expect { resolve_value }.not_to raise_error
+ end
+
+ it 'does not execute the extension' do
+ expect(described_class).not_to receive(:new)
+
+ resolve_value
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index b54c618d8e0..bf09e98331f 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -49,6 +49,31 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor))
end
+ before do
+ stub_feature_flags(graphql_keyset_pagination_without_next_page_query: false)
+ end
+
+ it 'invokes an extra query for the next page check' do
+ arguments[:first] = 1
+
+ subject.nodes
+
+ count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count
+ expect(count).to eq(1)
+ end
+
+ context 'when the relation is loaded' do
+ it 'invokes no extra query' do
+ allow(subject).to receive(:sliced_nodes).and_return(Project.all.to_a)
+ arguments[:first] = 1
+
+ subject.nodes
+
+ count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count
+ expect(count).to eq(0)
+ end
+ end
+
describe "with generic keyset order support" do
let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
@@ -412,4 +437,382 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
end
end
end
+
+ # duplicated tests, remove with the removal of the graphql_keyset_pagination_without_next_page_query FF
+ context 'when the graphql_keyset_pagination_without_next_page_query is on' do
+ let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
+
+ before do
+ stub_feature_flags(graphql_keyset_pagination_without_next_page_query: true)
+ end
+
+ it 'does not invoke an extra query for the next page check' do
+ arguments[:first] = 1
+
+ subject.nodes
+
+ count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count
+ expect(count).to eq(0)
+ end
+
+ it_behaves_like 'a connection with collection methods'
+
+ it_behaves_like 'a redactable connection' do
+ let_it_be(:projects) { create_list(:project, 2) }
+ let(:unwanted) { projects.second }
+ end
+
+ describe '#cursor_for' do
+ let(:project) { create(:project) }
+ let(:cursor) { connection.cursor_for(project) }
+
+ it 'returns an encoded ID' do
+ expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s)
+ end
+
+ context 'when an order is specified' do
+ let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
+
+ it 'returns the encoded value of the order' do
+ expect(decoded_cursor(cursor)).to include('id' => project.id.to_s)
+ end
+ end
+
+ context 'when multiple orders are specified' do
+ let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) }
+
+ it 'returns the encoded value of the order' do
+ expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect))
+ end
+ end
+ end
+
+ describe '#sliced_nodes' do
+ let(:projects) { create_list(:project, 4) }
+
+ context 'when before is passed' do
+ let(:arguments) { { before: encoded_cursor(projects[1]) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..])
+ end
+ end
+ end
+
+ context 'when after is passed' do
+ let(:arguments) { { after: encoded_cursor(projects[1]) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..])
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+ end
+ end
+
+ context 'when both before and after are passed' do
+ let(:arguments) do
+ {
+ after: encoded_cursor(projects[1]),
+ before: encoded_cursor(projects[3])
+ }
+ end
+
+ it 'returns the expected set' do
+ expect(subject.sliced_nodes).to contain_exactly(projects[2])
+ end
+ end
+
+ shared_examples 'nodes are in ascending order' do
+ context 'when no cursor is passed' do
+ let(:arguments) { {} }
+
+ it 'returns projects in ascending order' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes)
+ end
+ end
+
+ context 'when before cursor value is not NULL' do
+ let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes.first(2))
+ end
+ end
+
+ context 'when after cursor value is not NULL' do
+ let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes.last(3))
+ end
+ end
+
+ context 'when before and after cursor' do
+ let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes[1..3])
+ end
+ end
+ end
+
+ shared_examples 'nodes are in descending order' do
+ context 'when no cursor is passed' do
+ let(:arguments) { {} }
+
+ it 'only returns projects in descending order' do
+ expect(subject.sliced_nodes).to eq(descending_nodes)
+ end
+ end
+
+ context 'when before cursor value is not NULL' do
+ let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq(descending_nodes.first(2))
+ end
+ end
+
+ context 'when after cursor value is not NULL' do
+ let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(descending_nodes.last(3))
+ end
+ end
+
+ context 'when before and after cursor' do
+ let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(descending_nodes[1..3])
+ end
+ end
+ end
+
+ context 'when multiple orders with nil values are defined' do
+ let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3
+ let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1
+ let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5
+ let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2
+ let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4
+
+ context 'when ascending' do
+ let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) }
+ let_it_be(:nodes) { Project.order(order) }
+ let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] }
+
+ it_behaves_like 'nodes are in ascending order'
+
+ context 'when before cursor value is NULL' do
+ let(:arguments) { { before: encoded_cursor(project4) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq([project5, project1, project3, project2])
+ end
+ end
+
+ context 'when after cursor value is NULL' do
+ let(:arguments) { { after: encoded_cursor(project2) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project4])
+ end
+ end
+ end
+
+ context 'when descending' do
+ let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) }
+ let_it_be(:nodes) { Project.order(order) }
+ let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] }
+
+ it_behaves_like 'nodes are in descending order'
+
+ context 'when before cursor value is NULL' do
+ let(:arguments) { { before: encoded_cursor(project4) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq([project3, project1, project5, project2])
+ end
+ end
+
+ context 'when after cursor value is NULL' do
+ let(:arguments) { { after: encoded_cursor(project2) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project4])
+ end
+ end
+ end
+ end
+
+ context 'when ordering by similarity' do
+ let_it_be(:project1) { create(:project, name: 'test') }
+ let_it_be(:project2) { create(:project, name: 'testing') }
+ let_it_be(:project3) { create(:project, name: 'tests') }
+ let_it_be(:project4) { create(:project, name: 'testing stuff') }
+ let_it_be(:project5) { create(:project, name: 'test') }
+
+ let_it_be(:nodes) do
+ # Note: sorted_by_similarity_desc scope internally supports the generic keyset order.
+ Project.sorted_by_similarity_desc('test', include_in_select: true)
+ end
+
+ let_it_be(:descending_nodes) { nodes.to_a }
+
+ it_behaves_like 'nodes are in descending order'
+ end
+
+ context 'when an invalid cursor is provided' do
+ let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } }
+
+ it 'raises an error' do
+ expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+ end
+
+ describe '#nodes' do
+ let_it_be(:all_nodes) { create_list(:project, 5) }
+
+ let(:paged_nodes) { subject.nodes }
+
+ it_behaves_like 'connection with paged nodes' do
+ let(:paged_nodes_size) { 3 }
+ end
+
+ context 'when both are passed' do
+ let(:arguments) { { first: 2, last: 2 } }
+
+ it 'raises an error' do
+ expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
+ context 'when primary key is not in original order' do
+ let(:nodes) { Project.order(last_repository_check_at: :desc) }
+
+ it 'is added to end' do
+ sliced = subject.sliced_nodes
+
+ order_sql = sliced.order_values.last.to_sql
+
+ expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql)
+ end
+ end
+
+ context 'when there is no primary key' do
+ before do
+ stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base))
+ NoPrimaryKey.class_eval do
+ self.table_name = 'no_primary_key'
+ self.primary_key = nil
+ end
+ end
+
+ let(:nodes) { NoPrimaryKey.all }
+
+ it 'raises an error' do
+ expect(NoPrimaryKey.primary_key).to be_nil
+ expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key')
+ end
+ end
+ end
+
+ describe '#has_previous_page and #has_next_page' do
+ # using a list of 5 items with a max_page of 3
+ let_it_be(:project_list) { create_list(:project, 5) }
+ let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
+
+ context 'when default query' do
+ let(:arguments) { {} }
+
+ it 'has no previous, but a next' do
+ expect(subject.has_previous_page).to be_falsey
+ expect(subject.has_next_page).to be_truthy
+ end
+ end
+
+ context 'when before is first item' do
+ let(:arguments) { { before: encoded_cursor(project_list.first) } }
+
+ it 'has no previous, but a next' do
+ expect(subject.has_previous_page).to be_falsey
+ expect(subject.has_next_page).to be_truthy
+ end
+ end
+
+ describe 'using `before`' do
+ context 'when before is the last item' do
+ let(:arguments) { { before: encoded_cursor(project_list.last) } }
+
+ it 'has no previous, but a next' do
+ expect(subject.has_previous_page).to be_falsey
+ expect(subject.has_next_page).to be_truthy
+ end
+ end
+
+ context 'when before and last specified' do
+ let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } }
+
+ it 'has a previous and a next' do
+ expect(subject.has_previous_page).to be_truthy
+ expect(subject.has_next_page).to be_truthy
+ end
+ end
+
+ context 'when before and last does request all remaining nodes' do
+ let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } }
+
+ it 'has a previous and a next' do
+ expect(subject.has_previous_page).to be_falsey
+ expect(subject.has_next_page).to be_truthy
+ expect(subject.nodes).to eq [project_list[0]]
+ end
+ end
+ end
+
+ describe 'using `after`' do
+ context 'when after is the first item' do
+ let(:arguments) { { after: encoded_cursor(project_list.first) } }
+
+ it 'has a previous, and a next' do
+ expect(subject.has_previous_page).to be_truthy
+ expect(subject.has_next_page).to be_truthy
+ end
+ end
+
+ context 'when after and first specified' do
+ let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } }
+
+ it 'has a previous and a next' do
+ expect(subject.has_previous_page).to be_truthy
+ expect(subject.has_next_page).to be_truthy
+ end
+ end
+
+ context 'when before and last does request all remaining nodes' do
+ let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } }
+
+ it 'has a previous but no next' do
+ expect(subject.has_previous_page).to be_truthy
+ expect(subject.has_next_page).to be_falsey
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb
deleted file mode 100644
index 792cb03e8c7..00000000000
--- a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Graphql::Pagination::Keyset::LastItems do
- let_it_be(:merge_request) { create(:merge_request) }
-
- let(:scope) { MergeRequest.order_merged_at_asc }
-
- subject { described_class.take_items(*args) }
-
- context 'when the `count` parameter is nil' do
- let(:args) { [scope, nil] }
-
- it 'returns a single record' do
- expect(subject).to eq(merge_request)
- end
- end
-
- context 'when the `count` parameter is given' do
- let(:args) { [scope, 1] }
-
- it 'returns an array' do
- expect(subject).to eq([merge_request])
- end
- end
-end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 7c346e3eb69..000b8eff661 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::HealthChecks::GitalyCheck do
let(:result_class) { Gitlab::HealthChecks::Result }
@@ -14,20 +14,36 @@ RSpec.describe Gitlab::HealthChecks::GitalyCheck do
subject { described_class.readiness }
before do
- expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
+ expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(healthy_check)
end
context 'Gitaly server is up' do
- let(:gitaly_check) { double(check: { success: true }) }
+ before do
+ expect(Gitlab::GitalyClient::ServerService).to receive(:new).and_return(ready_check)
+ end
+
+ let(:healthy_check) { double(check: { success: true }) }
+ let(:ready_check) { double(readiness_check: { success: true }) }
it { is_expected.to eq([result_class.new('gitaly_check', true, nil, shard: 'default')]) }
end
context 'Gitaly server is down' do
- let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
+ let(:healthy_check) { double(check: { success: false, message: 'Connection refused' }) }
it { is_expected.to eq([result_class.new('gitaly_check', false, 'Connection refused', shard: 'default')]) }
end
+
+ context 'Gitaly server is not ready' do
+ before do
+ expect(Gitlab::GitalyClient::ServerService).to receive(:new).and_return(ready_check)
+ end
+
+ let(:healthy_check) { double(check: { success: true }) }
+ let(:ready_check) { double(readiness_check: { success: false, message: 'Clock is out of sync' }) }
+
+ it { is_expected.to match_array([result_class.new('gitaly_check', false, 'Clock is out of sync', shard: 'default')]) }
+ end
end
describe '#metrics' do
diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb
index 287ebcec207..8a87b01c560 100644
--- a/spec/lib/gitlab/health_checks/master_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/master_check_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
require_relative './simple_check_shared'
RSpec.describe Gitlab::HealthChecks::MasterCheck do
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
index 741c45d953c..f1791375cea 100644
--- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -12,18 +12,16 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
let(:checks) do
[
Gitlab::HealthChecks::DbCheck,
- Gitlab::HealthChecks::Redis::RedisCheck,
- Gitlab::HealthChecks::Redis::CacheCheck,
- Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::Redis::TraceChunksCheck,
- Gitlab::HealthChecks::Redis::RateLimitingCheck,
- Gitlab::HealthChecks::Redis::SessionsCheck,
+ *Gitlab::HealthChecks::Redis::ALL_INSTANCE_CHECKS,
Gitlab::HealthChecks::GitalyCheck
]
end
it 'responds with readiness checks data' do
+ expect_next_instance_of(Gitlab::GitalyClient::ServerService) do |service|
+ expect(service).to receive(:readiness_check).and_return({ success: true })
+ end
+
expect(subject.http_status).to eq(200)
expect(subject.json[:status]).to eq('ok')
@@ -37,8 +35,8 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
context 'when Redis fails' do
before do
- allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
- Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+ allow(Gitlab::HealthChecks::Redis::SharedStateCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('shared_state_check', false, "check error"))
end
it 'responds with failure' do
@@ -46,14 +44,14 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
expect(subject.json[:status]).to eq('failed')
expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
- expect(subject.json['redis_check']).to contain_exactly(
+ expect(subject.json['shared_state_check']).to contain_exactly(
status: 'failed', message: 'check error')
end
end
context 'when check raises exception not handled inside the check' do
before do
- expect(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_raise(
+ expect(Gitlab::HealthChecks::Redis::CacheCheck).to receive(:readiness).and_raise(
::Redis::CannotConnectError, 'Redis down')
end
diff --git a/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb b/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb
deleted file mode 100644
index c44bd2ed585..00000000000
--- a/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::CacheCheck do
- include_examples 'simple_check', 'redis_cache_ping', 'RedisCache', 'PONG'
-end
diff --git a/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb b/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb
deleted file mode 100644
index 3882e7db9d9..00000000000
--- a/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::QueuesCheck do
- include_examples 'simple_check', 'redis_queues_ping', 'RedisQueues', 'PONG'
-end
diff --git a/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb b/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb
deleted file mode 100644
index 1521fc99cde..00000000000
--- a/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::RateLimitingCheck do
- include_examples 'simple_check', 'redis_rate_limiting_ping', 'RedisRateLimiting', 'PONG'
-end
diff --git a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb
deleted file mode 100644
index 145d573b6de..00000000000
--- a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::RedisCheck do
- include_examples 'simple_check', 'redis_ping', 'Redis', true
-end
diff --git a/spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb b/spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb
deleted file mode 100644
index 82b3b33ec0a..00000000000
--- a/spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::SessionsCheck do
- include_examples 'simple_check', 'redis_sessions_ping', 'RedisSessions', 'PONG'
-end
diff --git a/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb b/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb
deleted file mode 100644
index 25917741a1c..00000000000
--- a/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::SharedStateCheck do
- include_examples 'simple_check', 'redis_shared_state_ping', 'RedisSharedState', 'PONG'
-end
diff --git a/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb b/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb
deleted file mode 100644
index 5fb5232a4dd..00000000000
--- a/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../simple_check_shared'
-
-RSpec.describe Gitlab::HealthChecks::Redis::TraceChunksCheck do
- include_examples 'simple_check', 'redis_trace_chunks_ping', 'RedisTraceChunks', 'PONG'
-end
diff --git a/spec/lib/gitlab/health_checks/redis_spec.rb b/spec/lib/gitlab/health_checks/redis_spec.rb
new file mode 100644
index 00000000000..2460f57a9ec
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/redis_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_relative './simple_check_shared'
+
+RSpec.describe Gitlab::HealthChecks::Redis do
+ describe "ALL_INSTANCE_CHECKS" do
+ subject { described_class::ALL_INSTANCE_CHECKS }
+
+ it { is_expected.to include(described_class::CacheCheck, described_class::QueuesCheck) }
+
+ it "contains a check for each redis instance" do
+ expect(subject.map(&:redis_instance_class_name)).to contain_exactly(*Gitlab::Redis::ALL_CLASSES)
+ end
+ end
+
+ describe 'all checks' do
+ described_class::ALL_INSTANCE_CHECKS.each do |check|
+ describe check do
+ include_examples 'simple_check',
+ "redis_#{check.redis_instance_class_name.store_name.underscore}_ping",
+ check.redis_instance_class_name.store_name,
+ 'PONG'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb
index 2f8816e62cc..fcdf3358570 100644
--- a/spec/lib/gitlab/i18n/metadata_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::I18n::MetadataEntry do
describe '#expected_forms' do
diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb
index f05346d07d3..df503e68cf1 100644
--- a/spec/lib/gitlab/i18n/translation_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#singular_translation' do
diff --git a/spec/lib/gitlab/import/merge_request_creator_spec.rb b/spec/lib/gitlab/import/merge_request_creator_spec.rb
index 9aedca40f1b..8f502216294 100644
--- a/spec/lib/gitlab/import/merge_request_creator_spec.rb
+++ b/spec/lib/gitlab/import/merge_request_creator_spec.rb
@@ -8,10 +8,13 @@ RSpec.describe Gitlab::Import::MergeRequestCreator do
subject { described_class.new(project) }
describe '#execute' do
+ let(:attributes) do
+ HashWithIndifferentAccess.new(merge_request.attributes.except('merge_params', 'suggested_reviewers'))
+ end
+
context 'merge request already exists' do
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:commits) { merge_request.merge_request_diffs.first.commits }
- let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) }
it 'updates the data' do
commits_count = commits.count
@@ -31,7 +34,6 @@ RSpec.describe Gitlab::Import::MergeRequestCreator do
context 'new merge request' do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
- let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) }
it 'creates a new merge request' do
attributes.delete(:id)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 9aec3271913..e270ca9ec6a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -211,6 +211,8 @@ merge_requests:
- user_note_authors
- cleanup_schedule
- compliance_violations
+- created_environments
+- predictions
external_pull_requests:
- project
merge_request_diff:
@@ -315,6 +317,7 @@ statuses:
- user
- auto_canceled_by
- needs
+- ci_stage
variables:
- project
triggers:
@@ -654,11 +657,9 @@ search_data:
merge_request_assignees:
- merge_request
- assignee
-- updated_state_by
merge_request_reviewers:
- merge_request
- reviewer
-- updated_state_by
lfs_file_locks:
- user
project_badges:
@@ -821,3 +822,28 @@ service_desk_setting:
approvals:
- user
- merge_request
+resource_milestone_events:
+ - user
+ - issue
+ - merge_request
+ - milestone
+resource_state_events:
+ - user
+ - issue
+ - merge_request
+ - source_merge_request
+ - epic
+iteration:
+ - group
+ - iterations_cadence
+ - issues
+ - labels
+ - merge_requests
+resource_iteration_events:
+ - user
+ - issue
+ - merge_request
+ - iteration
+iterations_cadence:
+ - group
+ - iterations
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 733be7fc226..272c2629b08 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::ImportExport::AttributeCleaner do
let(:relation_class) { double('relation_class').as_null_object }
diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
index 428d8d605ee..6536b895b2f 100644
--- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ merge_requests: {
include: [{ notes: { include: [{ author: { include: [] } }],
- preload: { author: nil } } }],
+ preload: { author: nil } } }],
preload: { notes: { author: nil } }
} }],
preload: { merge_requests: { notes: { author: nil } } }
@@ -132,7 +132,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
it 'generates the correct hash for a relation with included attributes' do
setup_yaml(tree: { project: [:issues] },
- included_attributes: { issues: [:name, :description] })
+ included_attributes: { issues: [:name, :description] })
is_expected.to match(
include: [{ issues: { include: [],
@@ -143,7 +143,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
it 'generates the correct hash for a relation with excluded attributes' do
setup_yaml(tree: { project: [:issues] },
- excluded_attributes: { issues: [:name] })
+ excluded_attributes: { issues: [:name] })
is_expected.to match(
include: [{ issues: { except: [:name],
@@ -154,8 +154,8 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
it 'generates the correct hash for a relation with both excluded and included attributes' do
setup_yaml(tree: { project: [:issues] },
- excluded_attributes: { issues: [:name] },
- included_attributes: { issues: [:description] })
+ excluded_attributes: { issues: [:name] },
+ included_attributes: { issues: [:description] })
is_expected.to match(
include: [{ issues: { except: [:name],
@@ -167,7 +167,7 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
it 'generates the correct hash for a relation with custom methods' do
setup_yaml(tree: { project: [:issues] },
- methods: { issues: [:name] })
+ methods: { issues: [:name] })
is_expected.to match(
include: [{ issues: { include: [],
diff --git a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb
index 9f1b15aa049..4ee825c71b6 100644
--- a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb
@@ -79,14 +79,14 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do
let(:relation_definition) { { 'notes' => {} } }
it 'saves valid subrelations and logs invalid subrelation' do
- expect(relation_object.notes).to receive(:<<).and_call_original
+ expect(relation_object.notes).to receive(:<<).twice.and_call_original
expect(Gitlab::Import::Logger)
.to receive(:info)
.with(
message: '[Project/Group Import] Invalid subrelation',
project_id: project.id,
relation_key: 'issues',
- error_messages: "Noteable can't be blank and Project does not match noteable project"
+ error_messages: "Project does not match noteable project"
)
saver.execute
@@ -94,9 +94,28 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do
issue = project.issues.last
import_failure = project.import_failures.last
+ expect(invalid_note.persisted?).to eq(false)
expect(issue.notes.count).to eq(5)
expect(import_failure.source).to eq('RelationObjectSaver#save!')
- expect(import_failure.exception_message).to eq("Noteable can't be blank and Project does not match noteable project")
+ expect(import_failure.exception_message).to eq('Project does not match noteable project')
+ end
+
+ context 'when invalid subrelation can still be persisted' do
+ let(:relation_key) { 'merge_requests' }
+ let(:relation_definition) { { 'approvals' => {} } }
+ let(:approval_1) { build(:approval, merge_request_id: nil, user: create(:user)) }
+ let(:approval_2) { build(:approval, merge_request_id: nil, user: create(:user)) }
+ let(:relation_object) { build(:merge_request, source_project: project, target_project: project, approvals: [approval_1, approval_2]) }
+
+ it 'saves the subrelation' do
+ expect(approval_1.valid?).to eq(false)
+ expect(Gitlab::Import::Logger).not_to receive(:info)
+
+ saver.execute
+
+ expect(project.merge_requests.first.approvals.count).to eq(2)
+ expect(project.merge_requests.first.approvals.first.persisted?).to eq(true)
+ end
end
context 'when importable is group' do
diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb
index fcb48678b88..8f848af8bd3 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -21,10 +21,12 @@ RSpec.describe Gitlab::ImportExport::Config do
end
it 'parses default config' do
+ expected_keys = [:tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders]
+ expected_keys << :include_if_exportable if ee
+
expect { subject }.not_to raise_error
expect(subject).to be_a(Hash)
- expect(subject.keys).to contain_exactly(
- :tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders)
+ expect(subject.keys).to match_array(expected_keys)
end
end
end
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
index 7b27f7183b0..5a75631ec4d 100644
--- a/spec/lib/gitlab/import_export/file_importer_spec.rb
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -169,7 +169,7 @@ RSpec.describe Gitlab::ImportExport::FileImporter do
end
it 'skips validation' do
- expect(subject).to receive(:validate_decompressed_archive_size).never
+ expect(subject).not_to receive(:validate_decompressed_archive_size)
subject.import
end
diff --git a/spec/lib/gitlab/import_export/group/object_builder_spec.rb b/spec/lib/gitlab/import_export/group/object_builder_spec.rb
index 09f40199b31..25d9858dd4c 100644
--- a/spec/lib/gitlab/import_export/group/object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/group/object_builder_spec.rb
@@ -6,9 +6,9 @@ RSpec.describe Gitlab::ImportExport::Group::ObjectBuilder do
let(:group) { create(:group) }
let(:base_attributes) do
{
- 'title' => 'title',
+ 'title' => 'title',
'description' => 'description',
- 'group' => group
+ 'group' => group
}
end
diff --git a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
index 2f1e2dd2db4..5e84284a060 100644
--- a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
@@ -33,15 +33,15 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
let(:relation_tree_restorer) do
described_class.new(
- user: user,
- shared: shared,
- relation_reader: relation_reader,
- object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
- members_mapper: members_mapper,
- relation_factory: Gitlab::ImportExport::Group::RelationFactory,
- reader: reader,
- importable: importable,
- importable_path: nil,
+ user: user,
+ shared: shared,
+ relation_reader: relation_reader,
+ object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
+ members_mapper: members_mapper,
+ relation_factory: Gitlab::ImportExport::Group::RelationFactory,
+ reader: reader,
+ importable: importable,
+ importable_path: nil,
importable_attributes: attributes
)
end
diff --git a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
index ab2c4cc2059..ed4368ba802 100644
--- a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::ImportExport::Json::LegacyWriter do
let(:path) { "#{Dir.tmpdir}/legacy_writer_spec/test.json" }
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index 3088129a732..02ac8065c9f 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -32,18 +32,20 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys }
let(:include) { [] }
let(:custom_orderer) { nil }
+ let(:include_if_exportable) { {} }
let(:relations_schema) do
{
only: [:name, :description],
include: include,
preload: { issues: nil },
- export_reorder: custom_orderer
+ export_reorder: custom_orderer,
+ include_if_exportable: include_if_exportable
}
end
subject do
- described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger)
+ described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger, current_user: user)
end
describe '#execute' do
@@ -210,11 +212,62 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
subject.execute
end
end
- end
- describe '.batch_size' do
- it 'returns default batch size' do
- expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
+ describe 'conditional export of included associations' do
+ let(:include) do
+ [{ issues: { include: [{ label_links: { include: [:label] } }] } }]
+ end
+
+ let(:include_if_exportable) do
+ { issues: [:label_links] }
+ end
+
+ let_it_be(:label) { create(:label, project: exportable) }
+ let_it_be(:link) { create(:label_link, label: label, target: issue) }
+
+ context 'when association is exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ end
+ end
+
+ it 'includes exportable association' do
+ expected_issue = issue.to_json(include: [{ label_links: { include: [:label] } }])
+
+ expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue))
+
+ subject.execute
+ end
+ end
+
+ context 'when association is not exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(false)
+ end
+ end
+
+ it 'filters out not exportable association' do
+ expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
+
+ subject.execute
+ end
+ end
+
+ context 'when association does not respond to exportable_association?' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
+ end
+ end
+
+ it 'filters out not exportable association' do
+ expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
index 0d372def8b0..c2c50751c3f 100644
--- a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do
it 'uses FastHashSerializer' do
expect(Gitlab::ImportExport::FastHashSerializer)
.to receive(:new)
- .with(exportable, tree, batch_size: Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE)
+ .with(exportable, tree)
.and_return(serializer)
expect(serializer).to receive(:execute)
diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
index b7b652005e9..ac646087a95 100644
--- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
@@ -21,15 +21,15 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:relation_tree_restorer) do
described_class.new(
- user: user,
- shared: shared,
- relation_reader: relation_reader,
- object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
- members_mapper: members_mapper,
- relation_factory: Gitlab::ImportExport::Project::RelationFactory,
- reader: reader,
- importable: importable,
- importable_path: 'project',
+ user: user,
+ shared: shared,
+ relation_reader: relation_reader,
+ object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
+ members_mapper: members_mapper,
+ relation_factory: Gitlab::ImportExport::Project::RelationFactory,
+ reader: reader,
+ importable: importable,
+ importable_path: 'project',
importable_attributes: attributes
)
end
diff --git a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
index 3dab84af744..d1fe9b80062 100644
--- a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
@@ -21,15 +21,15 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
let(:sample_data_relation_tree_restorer) do
described_class.new(
- user: user,
- shared: shared,
- relation_reader: relation_reader,
- object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
- members_mapper: members_mapper,
- relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory,
- reader: reader,
- importable: importable,
- importable_path: 'project',
+ user: user,
+ shared: shared,
+ relation_reader: relation_reader,
+ object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
+ members_mapper: members_mapper,
+ relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory,
+ reader: reader,
+ importable: importable,
+ importable_path: 'project',
importable_attributes: attributes
)
end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 47d7555c8f4..299e107c881 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -192,10 +192,26 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
expect(Issue.find_by(title: 'Voluptatem').resource_label_events).not_to be_empty
end
+ it 'restores issue resource milestone events' do
+ expect(Issue.find_by(title: 'Voluptatem').resource_milestone_events).not_to be_empty
+ end
+
+ it 'restores issue resource state events' do
+ expect(Issue.find_by(title: 'Voluptatem').resource_state_events).not_to be_empty
+ end
+
it 'restores merge requests resource label events' do
expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty
end
+ it 'restores merge request resource milestone events' do
+ expect(MergeRequest.find_by(title: 'MR1').resource_milestone_events).not_to be_empty
+ end
+
+ it 'restores merge request resource state events' do
+ expect(MergeRequest.find_by(title: 'MR1').resource_state_events).not_to be_empty
+ end
+
it 'restores suggestion' do
note = Note.find_by("note LIKE 'Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum%'")
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 6cfc24a8996..e591cbd05a0 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -586,6 +586,7 @@ ProjectFeature:
- environments_access_level
- feature_flags_access_level
- releases_access_level
+- monitor_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
@@ -706,7 +707,7 @@ ProtectedEnvironment:
- name
- created_at
- updated_at
-ProtectedEnvironment::DeployAccessLevel:
+ProtectedEnvironments::DeployAccessLevel:
- id
- protected_environment_id
- access_level
@@ -917,3 +918,29 @@ Approval:
- user_id
- created_at
- updated_at
+ResourceMilestoneEvent:
+ - user_id
+ - action
+ - state
+ - created_at
+ResourceStateEvent:
+ - user_id
+ - created_at
+ - state
+ - source_commit
+ - close_after_error_tracking_resolve
+ - close_auto_resolve_prometheus_alert
+Iteration:
+ - created_at
+ - updated_at
+ - start_date
+ - due_date
+ - group_id
+ - iid
+ - description
+ResourceIterationEvent:
+ - user_id
+ - created_at
+ - action
+Iterations::Cadence:
+ - title
diff --git a/spec/lib/gitlab/import_formatter_spec.rb b/spec/lib/gitlab/import_formatter_spec.rb
index fbf00ab92d3..0feff61725b 100644
--- a/spec/lib/gitlab/import_formatter_spec.rb
+++ b/spec/lib/gitlab/import_formatter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::ImportFormatter do
let(:formatter) { Gitlab::ImportFormatter.new }
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index f42a109aa3a..41ffcece221 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -7,17 +7,17 @@ RSpec.describe Gitlab::ImportSources do
it 'returns a hash' do
expected =
{
- 'GitHub' => 'github',
- 'Bitbucket Cloud' => 'bitbucket',
- 'Bitbucket Server' => 'bitbucket_server',
- 'GitLab.com' => 'gitlab',
- 'Google Code' => 'google_code',
- 'FogBugz' => 'fogbugz',
+ 'GitHub' => 'github',
+ 'Bitbucket Cloud' => 'bitbucket',
+ 'Bitbucket Server' => 'bitbucket_server',
+ 'GitLab.com' => 'gitlab',
+ 'Google Code' => 'google_code',
+ 'FogBugz' => 'fogbugz',
'Repository by URL' => 'git',
- 'GitLab export' => 'gitlab_project',
- 'Gitea' => 'gitea',
- 'Manifest file' => 'manifest',
- 'Phabricator' => 'phabricator'
+ 'GitLab export' => 'gitlab_project',
+ 'Gitea' => 'gitea',
+ 'Manifest file' => 'manifest',
+ 'Phabricator' => 'phabricator'
}
expect(described_class.options).to eq(expected)
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 72d201eed77..1545de6d8fd 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'fast_spec_helper'
RSpec.describe Gitlab::IncomingEmail do
describe "self.enabled?" do
diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb
index 3a281574563..f2bf06236b9 100644
--- a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb
+++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::InsecureKeyFingerprint do
let(:key) do
diff --git a/spec/lib/gitlab/instrumentation/redis_base_spec.rb b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
index a7e08b5a9bd..f9dd0c94c97 100644
--- a/spec/lib/gitlab/instrumentation/redis_base_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
@@ -65,6 +65,13 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
expect(instrumentation_class_b.get_request_count).to eq(2)
end
end
+
+ it 'increments by the given amount' do
+ instrumentation_class_a.increment_request_count(2)
+ instrumentation_class_a.increment_request_count(3)
+
+ expect(instrumentation_class_a.get_request_count).to eq(5)
+ end
end
describe '.increment_write_bytes' do
@@ -103,21 +110,21 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
context 'storage key overlapping' do
it 'keys do not overlap across storages' do
2.times do
- instrumentation_class_a.add_call_details(0.3, [:set])
- instrumentation_class_b.add_call_details(0.4, [:set])
+ instrumentation_class_a.add_call_details(0.3, [[:set]])
+ instrumentation_class_b.add_call_details(0.4, [[:set]])
end
expect(instrumentation_class_a.detail_store).to match(
[
- a_hash_including(cmd: :set, duration: 0.3, backtrace: an_instance_of(Array)),
- a_hash_including(cmd: :set, duration: 0.3, backtrace: an_instance_of(Array))
+ a_hash_including(commands: [[:set]], duration: 0.3, backtrace: an_instance_of(Array)),
+ a_hash_including(commands: [[:set]], duration: 0.3, backtrace: an_instance_of(Array))
]
)
expect(instrumentation_class_b.detail_store).to match(
[
- a_hash_including(cmd: :set, duration: 0.4, backtrace: an_instance_of(Array)),
- a_hash_including(cmd: :set, duration: 0.4, backtrace: an_instance_of(Array))
+ a_hash_including(commands: [[:set]], duration: 0.4, backtrace: an_instance_of(Array)),
+ a_hash_including(commands: [[:set]], duration: 0.4, backtrace: an_instance_of(Array))
]
)
end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 09280402e2b..5b5516f100b 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -47,11 +47,22 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
let(:instrumentation_class) { Gitlab::Redis::SharedState.instrumentation_class }
it 'counts successful requests' do
- expect(instrumentation_class).to receive(:instance_count_request).and_call_original
+ expect(instrumentation_class).to receive(:instance_count_request).with(1).and_call_original
Gitlab::Redis::SharedState.with { |redis| redis.call(:get, 'foobar') }
end
+ it 'counts successful pipelined requests' do
+ expect(instrumentation_class).to receive(:instance_count_request).with(2).and_call_original
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.call(:get, 'foobar')
+ pipeline.call(:get, 'foobarbaz')
+ end
+ end
+ end
+
it 'counts exceptions' do
expect(instrumentation_class).to receive(:instance_count_exception)
.with(instance_of(Redis::CommandError)).and_call_original
@@ -84,6 +95,20 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
end
end
+
+ context 'with pipelined commands' do
+ it 'measures requests that do not have blocking commands' do
+ expect(instrumentation_class).to receive(:instance_observe_duration).twice.with(a_value > 0)
+ .and_call_original
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.call(:get, 'foobar')
+ pipeline.call(:get, 'foobarbaz')
+ end
+ end
+ end
+ end
end
describe 'commands not in the apdex' do
@@ -109,6 +134,19 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
end
end
+
+ context 'with pipelined commands' do
+ it 'skips requests that have blocking commands', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373026' do
+ expect(instrumentation_class).not_to receive(:instance_observe_duration)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.call(:get, 'foo')
+ pipeline.call(:brpop, 'foobar', '0.01')
+ end
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb
index 900a079cdd2..c01d06c97b0 100644
--- a/spec/lib/gitlab/instrumentation/redis_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_spec.rb
@@ -71,14 +71,10 @@ RSpec.describe Gitlab::Instrumentation::Redis do
stub_storages(:detail_store, [details_row])
- expect(described_class.detail_store)
- .to contain_exactly(details_row.merge(storage: 'ActionCable'),
- details_row.merge(storage: 'Cache'),
- details_row.merge(storage: 'Queues'),
- details_row.merge(storage: 'SharedState'),
- details_row.merge(storage: 'TraceChunks'),
- details_row.merge(storage: 'RateLimiting'),
- details_row.merge(storage: 'Sessions'))
+ expected_detail_stores = Gitlab::Redis::ALL_CLASSES.map(&:store_name)
+ .map { |store_name| details_row.merge(storage: store_name) }
+ expected_detail_stores << details_row.merge(storage: 'ActionCable')
+ expect(described_class.detail_store).to contain_exactly(*expected_detail_stores)
end
end
end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 4fa9079144d..d5ff39767c4 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -140,13 +140,13 @@ RSpec.describe Gitlab::InstrumentationHelper do
subject
expect(payload).to include(db_replica_count: 0,
- db_replica_cached_count: 0,
- db_primary_count: 0,
- db_primary_cached_count: 0,
- db_primary_wal_count: 0,
- db_replica_wal_count: 0,
- db_primary_wal_cached_count: 0,
- db_replica_wal_cached_count: 0)
+ db_replica_cached_count: 0,
+ db_primary_count: 0,
+ db_primary_cached_count: 0,
+ db_primary_wal_count: 0,
+ db_replica_wal_count: 0,
+ db_primary_wal_cached_count: 0,
+ db_replica_wal_cached_count: 0)
end
context 'when replica caught up search was made' do
diff --git a/spec/lib/gitlab/internal_post_receive/response_spec.rb b/spec/lib/gitlab/internal_post_receive/response_spec.rb
index 135596c2de3..23ea5191486 100644
--- a/spec/lib/gitlab/internal_post_receive/response_spec.rb
+++ b/spec/lib/gitlab/internal_post_receive/response_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::InternalPostReceive::Response do
subject { described_class.new }
diff --git a/spec/lib/gitlab/jira/middleware_spec.rb b/spec/lib/gitlab/jira/middleware_spec.rb
index e7a79e40ac5..09cf67d0657 100644
--- a/spec/lib/gitlab/jira/middleware_spec.rb
+++ b/spec/lib/gitlab/jira/middleware_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Jira::Middleware do
let(:app) { double(:app) }
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Jira::Middleware do
describe '#call' do
it 'adjusts HTTP_AUTHORIZATION env when request from Jira DVCS user agent' do
expect(app).to receive(:call).with({ 'HTTP_USER_AGENT' => jira_user_agent,
- 'HTTP_AUTHORIZATION' => 'Bearer hash-123' })
+ 'HTTP_AUTHORIZATION' => 'Bearer hash-123' })
middleware.call('HTTP_USER_AGENT' => jira_user_agent, 'HTTP_AUTHORIZATION' => 'token hash-123')
end
diff --git a/spec/lib/gitlab/jira_import/metadata_collector_spec.rb b/spec/lib/gitlab/jira_import/metadata_collector_spec.rb
index 51751c7b75f..d8e31d0ae22 100644
--- a/spec/lib/gitlab/jira_import/metadata_collector_spec.rb
+++ b/spec/lib/gitlab/jira_import/metadata_collector_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::JiraImport::MetadataCollector do
describe '#execute' do
diff --git a/spec/lib/gitlab/jira_import_spec.rb b/spec/lib/gitlab/jira_import_spec.rb
index 972b0ab6ed1..c0c1a28b9ff 100644
--- a/spec/lib/gitlab/jira_import_spec.rb
+++ b/spec/lib/gitlab/jira_import_spec.rb
@@ -106,12 +106,6 @@ RSpec.describe Gitlab::JiraImport do
end
end
- describe '.jira_issue_cache_key' do
- it 'returns cache key for Jira issue imported to given project' do
- expect(described_class.jira_item_cache_key(project_id, 'DEMO-123', :issues)).to eq("jira-import/items-mapper/#{project_id}/issues/DEMO-123")
- end
- end
-
describe '.already_imported_cache_key' do
it 'returns cache key for already imported items' do
expect(described_class.already_imported_cache_key(:issues, project_id)).to eq("jira-importer/already-imported/#{project_id}/issues")
diff --git a/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb
index a3f0fd9eb9b..698b88c9fa1 100644
--- a/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Kubernetes::Helm::V2::Certificate do
describe '.generate_root' do
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb
index 508808be1be..549fd862d2d 100644
--- a/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::Cluster do
describe '#to_h' do
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb
index 43d4c46fda1..4734111a8ec 100644
--- a/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::Context do
describe '#to_h' do
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb
index 3d6acc80823..9eb6ddcf30c 100644
--- a/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::User do
describe '#to_h' do
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb
index 7d1f1aea291..869bba22a01 100644
--- a/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Kubernetes::Kubeconfig::Template do
let(:template) { described_class.new }
diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb
index 3e929cf200a..92907081867 100644
--- a/spec/lib/gitlab/lazy_spec.rb
+++ b/spec/lib/gitlab/lazy_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Lazy do
let(:dummy) { double(:dummy) }
diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb
index 83ba5858d81..08679b7e9f1 100644
--- a/spec/lib/gitlab/legacy_github_import/client_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb
@@ -98,6 +98,30 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do
end
end
+ describe '#repository' do
+ it 'returns repository data as a hash' do
+ stub_request(:get, 'https://api.github.com/rate_limit')
+ .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
+
+ stub_request(:get, 'https://api.github.com/repositories/1')
+ .to_return(status: 200, body: { id: 1 }.to_json, headers: { 'Content-Type' => 'application/json' })
+
+ expect(client.repository(1)).to eq({ id: 1 })
+ end
+ end
+
+ describe '#repos' do
+ it 'returns the user\'s repositories as a hash' do
+ stub_request(:get, 'https://api.github.com/rate_limit')
+ .to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
+
+ stub_request(:get, 'https://api.github.com/user/repos')
+ .to_return(status: 200, body: [{ id: 1 }, { id: 2 }].to_json, headers: { 'Content-Type' => 'application/json' })
+
+ expect(client.repos).to match_array([{ id: 1 }, { id: 2 }])
+ end
+ end
+
context 'github rate limit' do
it 'does not raise error when rate limit is disabled' do
stub_request(:get, /api.github.com/)
diff --git a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
index a5d2e00890b..a285a5820a2 100644
--- a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::LegacyGithubImport::IssuableFormatter do
let(:raw_data) do
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 68f1c214cef..17ecd183ac9 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -7,15 +7,15 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
let(:namespace) { create(:group) }
let(:repo) do
- ActiveSupport::InheritableOptions.new(
+ {
login: 'vim',
name: 'vim',
full_name: 'asd/vim',
clone_url: 'https://gitlab.com/asd/vim.git'
- )
+ }
end
- subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') }
+ subject(:service) { described_class.new(repo, repo[:name], namespace, user, github_access_token: 'asdffg') }
before do
namespace.add_owner(user)
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
context 'when GitHub project is private' do
it 'sets project visibility to private' do
- repo.private = true
+ repo[:private] = true
project = service.execute
@@ -50,17 +50,19 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
context 'when GitHub project is public' do
it 'sets project visibility to namespace visibility level' do
- repo.private = false
+ repo[:private] = false
+
project = service.execute
expect(project.visibility_level).to eq(namespace.visibility_level)
end
context 'when importing into a user namespace' do
- subject(:service) { described_class.new(repo, repo.name, user.namespace, user, github_access_token: 'asdffg') }
+ subject(:service) { described_class.new(repo, repo[:name], user.namespace, user, github_access_token: 'asdffg') }
it 'sets project visibility to user namespace visibility level' do
- repo.private = false
+ repo[:private] = false
+
project = service.execute
expect(project.visibility_level).to eq(user.namespace.visibility_level)
@@ -76,7 +78,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
end
it 'sets project visibility to the default project visibility' do
- repo.private = true
+ repo[:private] = true
project = service.execute
@@ -91,7 +93,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
end
it 'sets project visibility to the default project visibility' do
- repo.private = false
+ repo[:private] = false
project = service.execute
@@ -102,7 +104,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
context 'when GitHub project has wiki' do
it 'does not create the wiki repository' do
- allow(repo).to receive(:has_wiki?).and_return(true)
+ repo[:has_wiki] = true
project = service.execute
@@ -112,7 +114,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
context 'when GitHub project does not have wiki' do
it 'creates the wiki repository' do
- allow(repo).to receive(:has_wiki?).and_return(false)
+ repo[:has_wiki] = false
project = service.execute
diff --git a/spec/lib/gitlab/loop_helpers_spec.rb b/spec/lib/gitlab/loop_helpers_spec.rb
index 0535cb6068c..bb328e3dcce 100644
--- a/spec/lib/gitlab/loop_helpers_spec.rb
+++ b/spec/lib/gitlab/loop_helpers_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::LoopHelpers do
let(:class_instance) { (Class.new { include ::Gitlab::LoopHelpers }).new }
diff --git a/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb b/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb
index a2286415e96..4b9ea1c15a9 100644
--- a/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb
+++ b/spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb
@@ -20,18 +20,43 @@ RSpec.describe Gitlab::Mailgun::WebhookProcessors::FailureLogger do
context 'on permanent failure' do
let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'permanent' })) }
- it 'logs the failure immediately' do
- expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
- event: 'email_delivery_failure',
- mailgun_event_id: base_payload['id'],
- recipient: base_payload['recipient'],
- failure_type: 'permanent',
- failure_reason: base_payload['reason'],
- failure_code: base_payload['delivery-status']['code'],
- failure_message: base_payload['delivery-status']['message']
- )
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
+ .and_return(permanent_email_failure: { threshold: 1, interval: 1.minute })
+ end
- processor.execute
+ context 'when threshold is not exceeded' do
+ it 'increments counter but does not log the failure' do
+ expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(
+ :permanent_email_failure, scope: 'recipient@gitlab.com'
+ ).and_call_original
+ expect(Gitlab::ErrorTracking::Logger).not_to receive(:error)
+
+ processor.execute
+ end
+ end
+
+ context 'when threshold is exceeded' do
+ before do
+ processor.execute
+ end
+
+ it 'increments counter and logs the failure' do
+ expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(
+ :permanent_email_failure, scope: 'recipient@gitlab.com'
+ ).and_call_original
+ expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
+ event: 'email_delivery_failure',
+ mailgun_event_id: base_payload['id'],
+ recipient: base_payload['recipient'],
+ failure_type: 'permanent',
+ failure_reason: base_payload['reason'],
+ failure_code: base_payload['delivery-status']['code'],
+ failure_message: base_payload['delivery-status']['message']
+ )
+
+ processor.execute
+ end
end
end
diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
index 81910773dfa..57f2b1cfd96 100644
--- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
@@ -174,8 +174,8 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
expect(thing).to receive(:update_columns)
.with({ "title_html" => updated_html,
- "description_html" => "",
- "cached_markdown_version" => cache_version })
+ "description_html" => "",
+ "cached_markdown_version" => cache_version })
thing.refresh_markdown_cache!
end
diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
index b5d458f15fc..8e75009099d 100644
--- a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
@@ -62,7 +62,13 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach
it 'does not preload the markdown twice' do
expect(Gitlab::MarkdownCache::Redis::Store).to receive(:bulk_read).and_call_original
- expect(Gitlab::Redis::Cache).to receive(:with).twice.and_call_original
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:pipelined).and_call_original
+
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(:mapped_hmget).once.and_call_original
+ end
+ end
klass.preload_markdown_cache!([thing])
diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb
index bf5415ba1d7..2bffd029568 100644
--- a/spec/lib/gitlab/markup_helper_spec.rb
+++ b/spec/lib/gitlab/markup_helper_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::MarkupHelper do
describe '#markup?' do
diff --git a/spec/lib/gitlab/memory/jemalloc_spec.rb b/spec/lib/gitlab/memory/jemalloc_spec.rb
index 482ac6e5802..414d6017534 100644
--- a/spec/lib/gitlab/memory/jemalloc_spec.rb
+++ b/spec/lib/gitlab/memory/jemalloc_spec.rb
@@ -1,12 +1,15 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tmpdir'
RSpec.describe Gitlab::Memory::Jemalloc do
let(:outdir) { Dir.mktmpdir }
+ let(:tmp_outdir) { Dir.mktmpdir }
after do
FileUtils.rm_f(outdir)
+ FileUtils.rm_f(tmp_outdir)
end
context 'when jemalloc is loaded' do
@@ -28,7 +31,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do
describe '.dump_stats' do
it 'writes stats JSON file' do
- file_path = described_class.dump_stats(path: outdir, format: format)
+ file_path = described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir, format: format)
file = Dir.entries(outdir).find { |e| e.match(/jemalloc_stats\.#{$$}\.\d+\.json$/) }
expect(file).not_to be_nil
@@ -55,7 +58,8 @@ RSpec.describe Gitlab::Memory::Jemalloc do
describe '.dump_stats' do
shared_examples 'writes stats text file' do |filename_label, filename_pattern|
it do
- described_class.dump_stats(path: outdir, format: format, filename_label: filename_label)
+ described_class.dump_stats(
+ path: outdir, tmp_dir: tmp_outdir, format: format, filename_label: filename_label)
file = Dir.entries(outdir).find { |e| e.match(filename_pattern) }
expect(file).not_to be_nil
@@ -87,7 +91,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do
describe '.dump_stats' do
it 'raises an error' do
expect do
- described_class.dump_stats(path: outdir, format: format)
+ described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir, format: format)
end.to raise_error(/format must be one of/)
end
end
@@ -109,7 +113,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do
it 'does nothing' do
stub_env('LD_PRELOAD', nil)
- described_class.dump_stats(path: outdir)
+ described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir)
expect(Dir.empty?(outdir)).to be(true)
end
diff --git a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb
index 53fae48776b..b327a40bc2c 100644
--- a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb
+++ b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb
@@ -3,14 +3,19 @@
require 'spec_helper'
RSpec.describe Gitlab::Memory::Reports::JemallocStats do
- let(:reports_dir) { '/empty-dir' }
- let(:jemalloc_stats) { described_class.new(reports_path: reports_dir) }
+ let_it_be(:outdir) { Dir.mktmpdir }
+
+ let(:jemalloc_stats) { described_class.new(reports_path: outdir) }
+
+ after do
+ FileUtils.rm_f(outdir)
+ end
describe '.run' do
context 'when :report_jemalloc_stats ops FF is enabled' do
let(:worker_id) { 'puma_1' }
let(:report_name) { 'report.json' }
- let(:report_path) { File.join(reports_dir, report_name) }
+ let(:report_path) { File.join(outdir, report_name) }
before do
allow(Prometheus::PidProvider).to receive(:worker_id).and_return(worker_id)
@@ -18,14 +23,16 @@ RSpec.describe Gitlab::Memory::Reports::JemallocStats do
it 'invokes Jemalloc.dump_stats and returns file path' do
expect(Gitlab::Memory::Jemalloc)
- .to receive(:dump_stats).with(path: reports_dir, filename_label: worker_id).and_return(report_path)
+ .to receive(:dump_stats)
+ .with(path: outdir,
+ tmp_dir: File.join(outdir, '/tmp'),
+ filename_label: worker_id)
+ .and_return(report_path)
expect(jemalloc_stats.run).to eq(report_path)
end
describe 'reports cleanup' do
- let_it_be(:outdir) { Dir.mktmpdir }
-
let(:jemalloc_stats) { described_class.new(reports_path: outdir) }
before do
@@ -33,10 +40,6 @@ RSpec.describe Gitlab::Memory::Reports::JemallocStats do
allow(Gitlab::Memory::Jemalloc).to receive(:dump_stats)
end
- after do
- FileUtils.rm_f(outdir)
- end
-
context 'when number of reports exceeds `max_reports_stored`' do
let_it_be(:reports) do
now = Time.current
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
index 010f6884df3..beb49660022 100644
--- a/spec/lib/gitlab/memory/watchdog_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
+require_relative '../../../../lib/gitlab/cluster/lifecycle_events'
RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
context 'watchdog' do
@@ -8,23 +9,31 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
let(:handler) { instance_double(described_class::NullHandler) }
let(:heap_frag_limit_gauge) { instance_double(::Prometheus::Client::Gauge) }
- let(:heap_frag_violations_counter) { instance_double(::Prometheus::Client::Counter) }
- let(:heap_frag_violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:violations_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
let(:sleep_time) { 0.1 }
let(:max_heap_fragmentation) { 0.2 }
+ let(:max_mem_growth) { 2 }
+
+ # Defaults that will not trigger any events.
+ let(:fragmentation) { 0 }
+ let(:worker_memory) { 0 }
+ let(:primary_memory) { 0 }
+ let(:max_strikes) { 0 }
# Tests should set this to control the number of loop iterations in `call`.
let(:watchdog_iterations) { 1 }
subject(:watchdog) do
described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time,
- max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation).tap do |instance|
+ max_strikes: max_strikes, max_mem_growth: max_mem_growth,
+ max_heap_fragmentation: max_heap_fragmentation).tap do |instance|
# We need to defuse `sleep` and stop the internal loop after N iterations.
iterations = 0
- expect(instance).to receive(:sleep) do
- instance.stop if (iterations += 1) >= watchdog_iterations
- end.at_most(watchdog_iterations)
+ allow(instance).to receive(:sleep) do
+ instance.stop if (iterations += 1) > watchdog_iterations
+ end
end
end
@@ -33,34 +42,35 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
.with(:gitlab_memwd_heap_frag_limit, anything)
.and_return(heap_frag_limit_gauge)
allow(Gitlab::Metrics).to receive(:counter)
- .with(:gitlab_memwd_heap_frag_violations_total, anything, anything)
- .and_return(heap_frag_violations_counter)
+ .with(:gitlab_memwd_violations_total, anything, anything)
+ .and_return(violations_counter)
allow(Gitlab::Metrics).to receive(:counter)
- .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything)
- .and_return(heap_frag_violations_handled_counter)
+ .with(:gitlab_memwd_violations_handled_total, anything, anything)
+ .and_return(violations_handled_counter)
allow(heap_frag_limit_gauge).to receive(:set)
- allow(heap_frag_violations_counter).to receive(:increment)
- allow(heap_frag_violations_handled_counter).to receive(:increment)
+ allow(violations_counter).to receive(:increment)
+ allow(violations_handled_counter).to receive(:increment)
end
before do
stub_prometheus_metrics
- allow(handler).to receive(:on_high_heap_fragmentation).and_return(true)
+ allow(handler).to receive(:call).and_return(true)
allow(logger).to receive(:warn)
allow(logger).to receive(:info)
allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation)
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory })
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with(
+ pid: Gitlab::Cluster::PRIMARY_PID
+ ).and_return({ uss: primary_memory })
allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1')
end
context 'when created' do
- let(:fragmentation) { 0 }
- let(:max_strikes) { 0 }
-
it 'sets the heap fragmentation limit gauge' do
expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation)
@@ -71,7 +81,8 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
it 'initializes with defaults' do
watchdog = described_class.new(handler: handler, logger: logger)
- expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_HEAP_FRAG_THRESHOLD)
+ expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_MAX_HEAP_FRAG)
+ expect(watchdog.max_mem_growth).to eq(described_class::DEFAULT_MAX_MEM_GROWTH)
expect(watchdog.max_strikes).to eq(described_class::DEFAULT_MAX_STRIKES)
expect(watchdog.sleep_time_seconds).to eq(described_class::DEFAULT_SLEEP_TIME_SECONDS)
end
@@ -82,6 +93,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 1)
stub_env('GITLAB_MEMWD_MAX_STRIKES', 2)
stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 3)
+ stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 4)
end
it 'initializes with these settings' do
@@ -90,30 +102,17 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
expect(watchdog.max_heap_fragmentation).to eq(1)
expect(watchdog.max_strikes).to eq(2)
expect(watchdog.sleep_time_seconds).to eq(3)
+ expect(watchdog.max_mem_growth).to eq(4)
end
end
end
- context 'when process does not exceed heap fragmentation threshold' do
- let(:fragmentation) { max_heap_fragmentation - 0.1 }
- let(:max_strikes) { 0 } # To rule out that we were granting too many strikes.
-
- it 'does not signal the handler' do
- expect(handler).not_to receive(:on_high_heap_fragmentation)
-
- watchdog.call
- end
- end
-
- context 'when process exceeds heap fragmentation threshold permanently' do
- let(:fragmentation) { max_heap_fragmentation + 0.1 }
- let(:max_strikes) { 3 }
-
+ shared_examples 'has strikes left' do |stat|
context 'when process has not exceeded allowed number of strikes' do
let(:watchdog_iterations) { max_strikes }
it 'does not signal the handler' do
- expect(handler).not_to receive(:on_high_heap_fragmentation)
+ expect(handler).not_to receive(:call)
watchdog.call
end
@@ -125,119 +124,228 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
end
it 'increments the violations counter' do
- expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations)
+ expect(violations_counter).to receive(:increment).with(reason: stat).exactly(watchdog_iterations)
watchdog.call
end
it 'does not increment violations handled counter' do
- expect(heap_frag_violations_handled_counter).not_to receive(:increment)
+ expect(violations_handled_counter).not_to receive(:increment)
watchdog.call
end
end
+ end
+
+ shared_examples 'no strikes left' do |stat|
+ it 'signals the handler and resets strike counter' do
+ expect(handler).to receive(:call).and_return(true)
+
+ watchdog.call
+
+ expect(watchdog.strikes(stat.to_sym)).to eq(0)
+ end
+
+ it 'increments both the violations and violations handled counters' do
+ expect(violations_counter).to receive(:increment).with(reason: stat).exactly(watchdog_iterations)
+ expect(violations_handled_counter).to receive(:increment).with(reason: stat)
+
+ watchdog.call
+ end
- context 'when process exceeds the allowed number of strikes' do
- let(:watchdog_iterations) { max_strikes + 1 }
+ context 'when enforce_memory_watchdog ops toggle is off' do
+ before do
+ stub_feature_flags(enforce_memory_watchdog: false)
+ end
- it 'signals the handler and resets strike counter' do
- expect(handler).to receive(:on_high_heap_fragmentation).and_return(true)
+ it 'always uses the NullHandler' do
+ expect(handler).not_to receive(:call)
+ expect(described_class::NullHandler.instance).to receive(:call).and_return(true)
watchdog.call
+ end
+ end
- expect(watchdog.strikes).to eq(0)
+ context 'when handler result is true' do
+ it 'considers the event handled and stops itself' do
+ expect(handler).to receive(:call).once.and_return(true)
+ expect(logger).to receive(:info).with(hash_including(message: 'stopped'))
+
+ watchdog.call
end
+ end
- it 'logs the event' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
- expect(logger).to receive(:warn).with({
- message: 'heap fragmentation limit exceeded',
- pid: Process.pid,
- worker_id: 'worker_1',
- memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
- memwd_sleep_time_s: sleep_time,
- memwd_max_heap_frag: max_heap_fragmentation,
- memwd_cur_heap_frag: fragmentation,
- memwd_max_strikes: max_strikes,
- memwd_cur_strikes: max_strikes + 1,
- memwd_rss_bytes: 1024
- })
+ context 'when handler result is false' do
+ let(:max_strikes) { 0 } # to make sure the handler fires each iteration
+ let(:watchdog_iterations) { 3 }
+
+ it 'keeps running' do
+ expect(violations_counter).to receive(:increment).exactly(watchdog_iterations)
+ expect(violations_handled_counter).to receive(:increment).exactly(watchdog_iterations)
+ # Return true the third time to terminate the daemon.
+ expect(handler).to receive(:call).and_return(false, false, true)
watchdog.call
end
+ end
+ end
+
+ context 'when monitoring memory growth' do
+ let(:primary_memory) { 2048 }
- it 'increments both the violations and violations handled counters' do
- expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations)
- expect(heap_frag_violations_handled_counter).to receive(:increment)
+ context 'when process does not exceed threshold' do
+ let(:worker_memory) { max_mem_growth * primary_memory - 1 }
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:call)
watchdog.call
end
+ end
- context 'when enforce_memory_watchdog ops toggle is off' do
- before do
- stub_feature_flags(enforce_memory_watchdog: false)
- end
+ context 'when process exceeds threshold permanently' do
+ let(:worker_memory) { max_mem_growth * primary_memory + 1 }
+ let(:max_strikes) { 3 }
+
+ it_behaves_like 'has strikes left', 'mem_growth'
+
+ context 'when process exceeds the allowed number of strikes' do
+ let(:watchdog_iterations) { max_strikes + 1 }
- it 'always uses the NullHandler' do
- expect(handler).not_to receive(:on_high_heap_fragmentation)
- expect(described_class::NullHandler.instance).to(
- receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true)
- )
+ it_behaves_like 'no strikes left', 'mem_growth'
+
+ it 'only reads reference memory once' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss)
+ .with(pid: Gitlab::Cluster::PRIMARY_PID)
+ .once
watchdog.call
end
- end
- context 'when handler result is true' do
- it 'considers the event handled and stops itself' do
- expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true)
- expect(logger).to receive(:info).with(hash_including(message: 'stopped'))
+ it 'logs the event' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
+ expect(logger).to receive(:warn).with({
+ message: 'memory limit exceeded',
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
+ memwd_sleep_time_s: sleep_time,
+ memwd_max_uss_bytes: max_mem_growth * primary_memory,
+ memwd_ref_uss_bytes: primary_memory,
+ memwd_uss_bytes: worker_memory,
+ memwd_rss_bytes: 1024,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: max_strikes + 1
+ })
watchdog.call
end
end
+ end
+
+ context 'when process exceeds threshold temporarily' do
+ let(:worker_memory) { max_mem_growth * primary_memory }
+ let(:max_strikes) { 1 }
+ let(:watchdog_iterations) { 4 }
+
+ before do
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return(
+ { uss: worker_memory - 0.1 },
+ { uss: worker_memory + 0.2 },
+ { uss: worker_memory - 0.1 },
+ { uss: worker_memory + 0.1 }
+ )
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with(
+ pid: Gitlab::Cluster::PRIMARY_PID
+ ).and_return({ uss: primary_memory })
+ end
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:call)
+
+ watchdog.call
+ end
+ end
+ end
+
+ context 'when monitoring heap fragmentation' do
+ context 'when process does not exceed threshold' do
+ let(:fragmentation) { max_heap_fragmentation - 0.1 }
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:call)
+
+ watchdog.call
+ end
+ end
+
+ context 'when process exceeds threshold permanently' do
+ let(:fragmentation) { max_heap_fragmentation + 0.1 }
+ let(:max_strikes) { 3 }
- context 'when handler result is false' do
- let(:max_strikes) { 0 } # to make sure the handler fires each iteration
- let(:watchdog_iterations) { 3 }
+ it_behaves_like 'has strikes left', 'heap_frag'
- it 'keeps running' do
- expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations)
- expect(heap_frag_violations_handled_counter).to receive(:increment).exactly(watchdog_iterations)
- # Return true the third time to terminate the daemon.
- expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true)
+ context 'when process exceeds the allowed number of strikes' do
+ let(:watchdog_iterations) { max_strikes + 1 }
+
+ it_behaves_like 'no strikes left', 'heap_frag'
+
+ it 'logs the event' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
+ expect(logger).to receive(:warn).with({
+ message: 'heap fragmentation limit exceeded',
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
+ memwd_sleep_time_s: sleep_time,
+ memwd_max_heap_frag: max_heap_fragmentation,
+ memwd_cur_heap_frag: fragmentation,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: max_strikes + 1,
+ memwd_rss_bytes: 1024
+ })
watchdog.call
end
end
end
- end
- context 'when process exceeds heap fragmentation threshold temporarily' do
- let(:fragmentation) { max_heap_fragmentation }
- let(:max_strikes) { 1 }
- let(:watchdog_iterations) { 4 }
+ context 'when process exceeds threshold temporarily' do
+ let(:fragmentation) { max_heap_fragmentation }
+ let(:max_strikes) { 1 }
+ let(:watchdog_iterations) { 4 }
- before do
- allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(
- fragmentation - 0.1,
- fragmentation + 0.2,
- fragmentation - 0.1,
- fragmentation + 0.1
- )
+ before do
+ allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(
+ fragmentation - 0.1,
+ fragmentation + 0.2,
+ fragmentation - 0.1,
+ fragmentation + 0.1
+ )
+ end
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:call)
+
+ watchdog.call
+ end
end
+ end
- it 'does not signal the handler' do
- expect(handler).not_to receive(:on_high_heap_fragmentation)
+ context 'when both memory fragmentation and growth exceed thresholds' do
+ let(:fragmentation) { max_heap_fragmentation + 0.1 }
+ let(:primary_memory) { 2048 }
+ let(:worker_memory) { max_mem_growth * primary_memory + 1 }
+ let(:watchdog_iterations) { max_strikes + 1 }
+
+ it 'only calls the handler once' do
+ expect(handler).to receive(:call).once.and_return(true)
watchdog.call
end
end
context 'when gitlab_memory_watchdog ops toggle is off' do
- let(:fragmentation) { 0 }
- let(:max_strikes) { 0 }
-
before do
stub_feature_flags(gitlab_memory_watchdog: false)
end
@@ -247,6 +355,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
watchdog.call
end
+
+ it 'does not monitor memory growth' do
+ expect(Gitlab::Metrics::System).not_to receive(:memory_usage_uss_pss)
+
+ watchdog.call
+ end
end
end
@@ -254,9 +368,9 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
context 'NullHandler' do
subject(:handler) { described_class::NullHandler.instance }
- describe '#on_high_heap_fragmentation' do
+ describe '#call' do
it 'does nothing' do
- expect(handler.on_high_heap_fragmentation(1.0)).to be(false)
+ expect(handler.call).to be(false)
end
end
end
@@ -264,11 +378,11 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
context 'TermProcessHandler' do
subject(:handler) { described_class::TermProcessHandler.new(42) }
- describe '#on_high_heap_fragmentation' do
+ describe '#call' do
it 'sends SIGTERM to the current process' do
expect(Process).to receive(:kill).with(:TERM, 42)
- expect(handler.on_high_heap_fragmentation(1.0)).to be(true)
+ expect(handler.call).to be(true)
end
end
end
@@ -286,12 +400,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class)
end
- describe '#on_high_heap_fragmentation' do
+ describe '#call' do
it 'invokes orderly termination via Puma API' do
expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle)
expect(puma_worker_handle).to receive(:term)
- expect(handler.on_high_heap_fragmentation(1.0)).to be(true)
+ expect(handler.call).to be(true)
end
end
end
diff --git a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb
index ed11f8ea6bb..0e8b598730c 100644
--- a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb
+++ b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::MergeRequests::Mergeability::ResultsStore do
subject(:results_store) { described_class.new(merge_request: merge_request, interface: interface) }
diff --git a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb
index 1f306753c39..b8556829a59 100644
--- a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Metrics::Dashboard::Defaults do
it { is_expected.to be_const_defined(:DEFAULT_PANEL_TYPE) }
diff --git a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb
index c15e717b126..bc6cd383758 100644
--- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb
@@ -24,13 +24,13 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
context 'with existing metrics' do
let(:existing_metric_attributes) do
{
- project: project,
- identifier: 'metric_b',
- title: 'overwrite',
- y_label: 'overwrite',
- query: 'overwrite',
- unit: 'overwrite',
- legend: 'overwrite',
+ project: project,
+ identifier: 'metric_b',
+ title: 'overwrite',
+ y_label: 'overwrite',
+ query: 'overwrite',
+ unit: 'overwrite',
+ legend: 'overwrite',
dashboard_path: dashboard_path
}
end
@@ -43,11 +43,11 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
subject.execute
expect(existing_metric.reload.attributes.with_indifferent_access).to include({
- title: 'Super Chart B',
+ title: 'Super Chart B',
y_label: 'y_label',
- query: 'query',
- unit: 'unit',
- legend: 'Legend Label'
+ query: 'query',
+ unit: 'unit',
+ legend: 'Legend Label'
})
end
@@ -69,11 +69,11 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
subject.execute
expect(existing_metric.reload.attributes.with_indifferent_access).to include({
- title: 'Super Chart B',
+ title: 'Super Chart B',
y_label: 'y_label',
- query: 'query',
- unit: 'unit',
- legend: 'Legend Label'
+ query: 'query',
+ unit: 'unit',
+ legend: 'Legend Label'
})
end
diff --git a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb
index fdbba6c31b5..a50c2a506cb 100644
--- a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb
@@ -17,11 +17,11 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:error_hash) do
{
- 'data' => 'property_name',
+ 'data' => 'property_name',
'data_pointer' => pointer,
- 'type' => type,
- 'schema' => 'schema',
- 'details' => details
+ 'type' => type,
+ 'schema' => 'schema',
+ 'details' => details
}
end
@@ -72,10 +72,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:type) { 'pattern' }
let(:error_hash) do
{
- 'data' => 'property_name',
+ 'data' => 'property_name',
'data_pointer' => pointer,
- 'type' => type,
- 'schema' => { 'pattern' => 'aa.*' }
+ 'type' => type,
+ 'schema' => { 'pattern' => 'aa.*' }
}
end
@@ -86,10 +86,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:type) { 'format' }
let(:error_hash) do
{
- 'data' => 'property_name',
+ 'data' => 'property_name',
'data_pointer' => pointer,
- 'type' => type,
- 'schema' => { 'format' => 'date-time' }
+ 'type' => type,
+ 'schema' => { 'format' => 'date-time' }
}
end
@@ -100,10 +100,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:type) { 'const' }
let(:error_hash) do
{
- 'data' => 'property_name',
+ 'data' => 'property_name',
'data_pointer' => pointer,
- 'type' => type,
- 'schema' => { 'const' => 'one' }
+ 'type' => type,
+ 'schema' => { 'const' => 'one' }
}
end
@@ -114,10 +114,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:type) { 'enum' }
let(:error_hash) do
{
- 'data' => 'property_name',
+ 'data' => 'property_name',
'data_pointer' => pointer,
- 'type' => type,
- 'schema' => { 'enum' => %w(one two) }
+ 'type' => type,
+ 'schema' => { 'enum' => %w(one two) }
}
end
@@ -128,10 +128,10 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
let(:type) { 'unknown' }
let(:error_hash) do
{
- 'data' => 'property_name',
+ 'data' => 'property_name',
'data_pointer' => pointer,
- 'type' => type,
- 'schema' => 'schema'
+ 'type' => type,
+ 'schema' => 'schema'
}
end
diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb
index eb67ea2b7da..aaa9daf8fee 100644
--- a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb
@@ -33,9 +33,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
context 'with metric identifier present in current dashboard' do
before do
create(:prometheus_metric,
- identifier: 'metric_a1',
+ identifier: 'metric_a1',
dashboard_path: 'test/path.yml',
- project: project
+ project: project
)
end
@@ -45,9 +45,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
context 'with metric identifier present in another dashboard' do
before do
create(:prometheus_metric,
- identifier: 'metric_a1',
+ identifier: 'metric_a1',
dashboard_path: 'some/other/dashboard/path.yml',
- project: project
+ project: project
)
end
@@ -94,9 +94,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
context 'with metric identifier present in current dashboard' do
before do
create(:prometheus_metric,
- identifier: 'metric_a1',
+ identifier: 'metric_a1',
dashboard_path: 'test/path.yml',
- project: project
+ project: project
)
end
@@ -106,9 +106,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
context 'with metric identifier present in another dashboard' do
before do
create(:prometheus_metric,
- identifier: 'metric_a1',
+ identifier: 'metric_a1',
dashboard_path: 'some/other/dashboard/path.yml',
- project: project
+ project: project
)
end
@@ -166,9 +166,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
context 'with metric identifier present in current dashboard' do
before do
create(:prometheus_metric,
- identifier: 'metric_a1',
+ identifier: 'metric_a1',
dashboard_path: 'test/path.yml',
- project: project
+ project: project
)
end
@@ -178,9 +178,9 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
context 'with metric identifier present in another dashboard' do
before do
create(:prometheus_metric,
- identifier: 'metric_a1',
+ identifier: 'metric_a1',
dashboard_path: 'some/other/dashboard/path.yml',
- project: project
+ project: project
)
end
diff --git a/spec/lib/gitlab/metrics/delta_spec.rb b/spec/lib/gitlab/metrics/delta_spec.rb
index e768da875c2..fdbb5e4ce4d 100644
--- a/spec/lib/gitlab/metrics/delta_spec.rb
+++ b/spec/lib/gitlab/metrics/delta_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Metrics::Delta do
let(:delta) { described_class.new }
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index dc5c7eb2e55..fa50adb4e4f 100644
--- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -10,11 +10,12 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
describe 'when exporter is enabled' do
before do
allow(::WEBrick::HTTPServer).to receive(:new).with(
- Port: anything,
- BindAddress: anything,
- Logger: anything,
- AccessLog: anything
- ).and_call_original
+ {
+ Port: anything,
+ BindAddress: anything,
+ Logger: anything,
+ AccessLog: anything
+ }).and_call_original
allow(settings).to receive(:enabled).and_return(true)
allow(settings).to receive(:port).and_return(0)
@@ -45,11 +46,12 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
it 'starts server with port and address from settings' do
expect(::WEBrick::HTTPServer).to receive(:new).with(
- Port: port,
- BindAddress: address,
- Logger: anything,
- AccessLog: anything
- ).and_wrap_original do |m, *args|
+ {
+ Port: port,
+ BindAddress: address,
+ Logger: anything,
+ AccessLog: anything
+ }).and_wrap_original do |m, *args|
m.call(DoNotListen: true, Logger: args.first[:Logger])
end
diff --git a/spec/lib/gitlab/metrics/global_search_slis_spec.rb b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
new file mode 100644
index 00000000000..28496eff2fc
--- /dev/null
+++ b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:apdex_feature_flag_enabled) { true }
+ let(:error_rate_feature_flag_enabled) { true }
+
+ before do
+ stub_feature_flags(global_search_custom_slis: apdex_feature_flag_enabled)
+ stub_feature_flags(global_search_error_rate_sli: error_rate_feature_flag_enabled)
+ end
+
+ describe '#initialize_slis!' do
+ context 'when global_search_custom_slis feature flag is enabled' do
+ let(:apdex_feature_flag_enabled) { true }
+
+ it 'initializes Apdex SLIs for global_search' do
+ expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(
+ :global_search,
+ a_kind_of(Array)
+ )
+
+ described_class.initialize_slis!
+ end
+ end
+
+ context 'when global_search_error_rate_sli feature flag is enabled' do
+ let(:error_rate_feature_flag_enabled) { true }
+
+ it 'initializes ErrorRate SLIs for global_search' do
+ expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(
+ :global_search,
+ a_kind_of(Array)
+ )
+
+ described_class.initialize_slis!
+ end
+ end
+
+ context 'when global_search_custom_slis feature flag is disabled' do
+ let(:apdex_feature_flag_enabled) { false }
+
+ it 'does not initialize the Apdex SLIs for global_search' do
+ expect(Gitlab::Metrics::Sli::Apdex).not_to receive(:initialize_sli)
+
+ described_class.initialize_slis!
+ end
+ end
+
+ context 'when global_search_error_rate_sli feature flag is disabled' do
+ let(:error_rate_feature_flag_enabled) { false }
+
+ it 'does not initialize the ErrorRate SLIs for global_search' do
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to receive(:initialize_sli)
+
+ described_class.initialize_slis!
+ end
+ end
+ end
+
+ describe '#record_apdex' do
+ context 'when global_search_custom_slis feature flag is enabled' do
+ let(:apdex_feature_flag_enabled) { true }
+
+ where(:search_type, :code_search, :duration_target) do
+ 'basic' | false | 7.031
+ 'basic' | true | 21.903
+ 'advanced' | false | 4.865
+ 'advanced' | true | 13.546
+ end
+
+ with_them do
+ before do
+ allow(::Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return('end')
+ end
+
+ let(:search_scope) { code_search ? 'blobs' : 'issues' }
+
+ it 'increments the global_search SLI as a success if the elapsed time is within the target' do
+ duration = duration_target - 0.1
+
+ expect(Gitlab::Metrics::Sli::Apdex[:global_search]).to receive(:increment).with(
+ labels: {
+ search_type: search_type,
+ search_level: 'global',
+ search_scope: search_scope,
+ endpoint_id: 'end'
+ },
+ success: true
+ )
+
+ described_class.record_apdex(
+ elapsed: duration,
+ search_type: search_type,
+ search_level: 'global',
+ search_scope: search_scope
+ )
+ end
+
+ it 'increments the global_search SLI as a failure if the elapsed time is not within the target' do
+ duration = duration_target + 0.1
+
+ expect(Gitlab::Metrics::Sli::Apdex[:global_search]).to receive(:increment).with(
+ labels: {
+ search_type: search_type,
+ search_level: 'global',
+ search_scope: search_scope,
+ endpoint_id: 'end'
+ },
+ success: false
+ )
+
+ described_class.record_apdex(
+ elapsed: duration,
+ search_type: search_type,
+ search_level: 'global',
+ search_scope: search_scope
+ )
+ end
+ end
+ end
+
+ context 'when global_search_custom_slis feature flag is disabled' do
+ let(:apdex_feature_flag_enabled) { false }
+
+ it 'does not call increment on the apdex SLI' do
+ expect(Gitlab::Metrics::Sli::Apdex[:global_search]).not_to receive(:increment)
+
+ described_class.record_apdex(
+ elapsed: 1,
+ search_type: 'basic',
+ search_level: 'global',
+ search_scope: 'issues'
+ )
+ end
+ end
+ end
+
+ describe '#record_error_rate' do
+ context 'when global_search_error_rate_sli feature flag is enabled' do
+ let(:error_rate_feature_flag_enabled) { true }
+
+ it 'calls increment on the error rate SLI' do
+ expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).to receive(:increment)
+
+ described_class.record_error_rate(
+ error: true,
+ search_type: 'basic',
+ search_level: 'global',
+ search_scope: 'issues'
+ )
+ end
+ end
+
+ context 'when global_search_error_rate_sli feature flag is disabled' do
+ let(:error_rate_feature_flag_enabled) { false }
+
+ it 'does not call increment on the error rate SLI' do
+ expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).not_to receive(:increment)
+
+ described_class.record_error_rate(
+ error: true,
+ search_type: 'basic',
+ search_level: 'global',
+ search_scope: 'issues'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index ab56f38f0c1..21028d18648 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Metrics::RackMiddleware do
let(:app) { double(:app) }
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index 3396de9b12c..ed78548ef62 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -194,9 +194,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
let(:endpoint) do
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
- double(:endpoint, route: route,
- options: { for: api_handler, path: [":id/archive"] },
- namespace: "/projects")
+ double(:endpoint,
+ route: route, options: { for: api_handler, path: [":id/archive"] }, namespace: "/projects")
end
let(:env) { { 'api.endpoint' => endpoint, 'REQUEST_METHOD' => 'GET' } }
@@ -256,9 +255,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
context 'Grape API without expected duration' do
let(:endpoint) do
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
- double(:endpoint, route: route,
- options: { for: api_handler, path: [":id/archive"] },
- namespace: "/projects")
+ double(:endpoint,
+ route: route, options: { for: api_handler, path: [":id/archive"] }, namespace: "/projects")
end
let(:env) { { 'api.endpoint' => endpoint, 'REQUEST_METHOD' => 'GET' } }
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
index adbc474343f..67cd8630758 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActionView do
root = Rails.root.to_s
double(:event, duration: 2.1,
- payload: { identifier: "#{root}/app/views/x.html.haml" })
+ payload: { identifier: "#{root}/app/views/x.html.haml" })
end
before do
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 28c3ef229ab..005c1ae2d0a 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -137,7 +137,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
:event,
name: 'transaction.active_record',
duration: 230,
- payload: { connection: connection }
+ payload: { connection: connection }
)
end
@@ -213,7 +213,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
:event,
name: 'sql.active_record',
duration: 2,
- payload: payload
+ payload: payload
)
end
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
:event,
name: 'sql.active_record',
duration: 2,
- payload: payload
+ payload: payload
)
end
diff --git a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb
index bc6effd0438..7f7efaffd9e 100644
--- a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::LoadBalancing, :request_store do
double(
:event,
name: 'load_balancing.caught_up_replica_pick',
- payload: payload
+ payload: payload
)
end
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::LoadBalancing, :request_store do
double(
:event,
name: 'load_balancing.web_transaction_completed',
- payload: {}
+ payload: {}
)
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index ce3caf8cdfe..7739501dd95 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Metrics::System do
context 'when /proc files exist' do
@@ -72,10 +72,20 @@ RSpec.describe Gitlab::Metrics::System do
end
describe '.memory_usage_rss' do
- it "returns the process' resident set size (RSS) in bytes" do
- mock_existing_proc_file('/proc/self/status', proc_status)
+ context 'without PID' do
+ it "returns the current process' resident set size (RSS) in bytes" do
+ mock_existing_proc_file('/proc/self/status', proc_status)
+
+ expect(described_class.memory_usage_rss).to eq(2527232)
+ end
+ end
+
+ context 'with PID' do
+ it "returns the given process' resident set size (RSS) in bytes" do
+ mock_existing_proc_file('/proc/7/status', proc_status)
- expect(described_class.memory_usage_rss).to eq(2527232)
+ expect(described_class.memory_usage_rss(pid: 7)).to eq(2527232)
+ end
end
end
@@ -96,11 +106,22 @@ RSpec.describe Gitlab::Metrics::System do
end
describe '.memory_usage_uss_pss' do
- it "returns the process' unique and porportional set size (USS/PSS) in bytes" do
- mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup)
+ context 'without PID' do
+ it "returns the current process' unique and porportional set size (USS/PSS) in bytes" do
+ mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup)
+
+ # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024
+ expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072)
+ end
+ end
+
+ context 'with PID' do
+ it "returns the given process' unique and porportional set size (USS/PSS) in bytes" do
+ mock_existing_proc_file('/proc/7/smaps_rollup', proc_smaps_rollup)
- # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024
- expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072)
+ # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024
+ expect(described_class.memory_usage_uss_pss(pid: 7)).to eq(uss: 475136, pss: 515072)
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index b1c15db5193..1a8538b5d6a 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Metrics::Transaction do
describe '#run' do
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index d6590efcf4f..dc59fa804c4 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -66,8 +66,8 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
before do
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
endpoint = double(:endpoint, route: route,
- options: { for: API::Projects, path: [":id/archive"] },
- namespace: "/projects")
+ options: { for: API::Projects, path: [":id/archive"] },
+ namespace: "/projects")
env['api.endpoint'] = endpoint
diff --git a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb
index b868207e67c..02c4ea4df27 100644
--- a/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb
+++ b/spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb
@@ -2,6 +2,7 @@
require 'fast_spec_helper'
require 'rack'
+require 'tempfile'
RSpec.describe Gitlab::Middleware::RackMultipartTempfileFactory do
let(:app) do
diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb
index ca0ec0b9d83..a5bda23b38b 100644
--- a/spec/lib/gitlab/middleware/release_env_spec.rb
+++ b/spec/lib/gitlab/middleware/release_env_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Middleware::ReleaseEnv do
let(:inner_app) { double(:app, call: 'yay') }
diff --git a/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb b/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb
index 91c030a0f45..9fb56e45103 100644
--- a/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb
+++ b/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Middleware::SidekiqWebStatic do
let(:app) { double(:app) }
diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb
index a569c86960c..2c258ce3da6 100644
--- a/spec/lib/gitlab/namespaced_session_store_spec.rb
+++ b/spec/lib/gitlab/namespaced_session_store_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::NamespacedSessionStore do
let(:key) { :some_key }
diff --git a/spec/lib/gitlab/nav/top_nav_menu_header_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_header_spec.rb
new file mode 100644
index 00000000000..d9da3ba1e46
--- /dev/null
+++ b/spec/lib/gitlab/nav/top_nav_menu_header_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe ::Gitlab::Nav::TopNavMenuHeader do
+ describe '.build' do
+ it 'builds a hash from with the given header' do
+ title = 'Test Header'
+ expected = {
+ title: title,
+ type: :header
+ }
+ expect(described_class.build(title: title)).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
index 966b23bf51a..d1d6ac80c40 100644
--- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
+++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe ::Gitlab::Nav::TopNavMenuItem do
describe '.build' do
@@ -17,7 +17,7 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem do
emoji: 'smile'
}
- expect(described_class.build(**item)).to eq(item)
+ expect(described_class.build(**item)).to eq(item.merge(type: :item))
end
end
end
diff --git a/spec/lib/gitlab/net_http_adapter_spec.rb b/spec/lib/gitlab/net_http_adapter_spec.rb
index 21c1a1ebe25..fdaf35be31e 100644
--- a/spec/lib/gitlab/net_http_adapter_spec.rb
+++ b/spec/lib/gitlab/net_http_adapter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::NetHttpAdapter do
describe '#connect' do
diff --git a/spec/lib/gitlab/null_request_store_spec.rb b/spec/lib/gitlab/null_request_store_spec.rb
index 66700313c9a..f68f478c73e 100644
--- a/spec/lib/gitlab/null_request_store_spec.rb
+++ b/spec/lib/gitlab/null_request_store_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::NullRequestStore do
let(:null_store) { described_class.new }
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index c91b14a33ba..563c97fa2cb 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
context 'when there is an app_id and an app_secret, and an array of args' do
let(:provider) do
{
- 'name' => 'unknown',
+ 'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => %w[one two three]
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
context 'when there is an app_id and an app_secret, and an array of args, and default values' do
let(:provider) do
{
- 'name' => 'unknown',
+ 'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => %w[one two three]
@@ -68,7 +68,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
context 'when there is an app_id and an app_secret, and a hash of args' do
let(:provider) do
{
- 'name' => 'unknown',
+ 'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
context 'when there is an app_id and an app_secret, and a hash of args, and default arguments' do
let(:provider) do
{
- 'name' => 'unknown',
+ 'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
@@ -106,7 +106,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
context 'when there is an app_id and an app_secret, no args, and default values' do
let(:provider) do
{
- 'name' => 'unknown',
+ 'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2
}
@@ -127,7 +127,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
context 'when there are args, of an unsupported type' do
let(:provider) do
{
- 'name' => 'unknown',
+ 'name' => 'unknown',
'args' => 1
}
end
diff --git a/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb b/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb
index 778244677ef..100574cc75f 100644
--- a/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb
@@ -50,6 +50,20 @@ RSpec.describe Gitlab::Pagination::Keyset::ColumnOrderDefinition do
it { expect(project_calculated_column).to be_ascending_order }
it { expect(project_calculated_column).not_to be_descending_order }
+ context 'when order expression is an Arel node with nulls_last' do
+ it 'can automatically determine the reversed expression' do
+ column_order_definition = described_class.new(
+ attribute_name: :name,
+ column_expression: Project.arel_table[:name],
+ order_expression: Project.arel_table[:name].asc.nulls_last,
+ nullable: :nulls_last,
+ distinct: false
+ )
+
+ expect(column_order_definition).to be_ascending_order
+ end
+ end
+
it 'raises error when order direction cannot be infered' do
expect do
described_class.new(
@@ -132,6 +146,21 @@ RSpec.describe Gitlab::Pagination::Keyset::ColumnOrderDefinition do
expect(column_order_definition.reverse.order_expression).to eq('name desc')
end
end
+
+ context 'when order expression is an Arel node with nulls_last' do
+ it 'can automatically determine the reversed expression' do
+ column_order_definition = described_class.new(
+ attribute_name: :name,
+ column_expression: Project.arel_table[:name],
+ order_expression: Project.arel_table[:name].asc.nulls_last,
+ order_direction: :asc,
+ nullable: :nulls_last,
+ distinct: false
+ )
+
+ expect(column_order_definition.reverse.order_expression).to eq(Project.arel_table[:name].desc.nulls_first)
+ end
+ end
end
describe '#nullable' do
diff --git a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
index 25a52af3a7a..2b8570e4aff 100644
--- a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::PhabricatorImport::Representation::Task do
subject(:task) do
diff --git a/spec/lib/gitlab/phabricator_import/representation/user_spec.rb b/spec/lib/gitlab/phabricator_import/representation/user_spec.rb
index f51be0f7d8d..6df26b905cc 100644
--- a/spec/lib/gitlab/phabricator_import/representation/user_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/representation/user_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::PhabricatorImport::Representation::User do
subject(:user) do
diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb
index c7b64e8108b..eacb63c8f8a 100644
--- a/spec/lib/gitlab/popen/runner_spec.rb
+++ b/spec/lib/gitlab/popen/runner_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Popen::Runner do
subject { described_class.new }
diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb
index 8f43943e2d1..3ff1c8e9012 100644
--- a/spec/lib/gitlab/push_options_spec.rb
+++ b/spec/lib/gitlab/push_options_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::PushOptions do
describe 'namespace and key validation' do
diff --git a/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb b/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb
index 8a4e9ab8bb7..08bb06150d4 100644
--- a/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb
+++ b/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::QuickActions::SubstitutionDefinition do
let(:content) do
diff --git a/spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb b/spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb
new file mode 100644
index 00000000000..89fe19b8f60
--- /dev/null
+++ b/spec/lib/gitlab/quick_actions/timeline_text_and_date_time_separator_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::QuickActions::TimelineTextAndDateTimeSeparator do
+ subject(:timeline_text_and_datetime_separator) { described_class }
+
+ shared_examples 'arg line with invalid parameters' do
+ it 'returns nil' do
+ expect(timeline_text_and_datetime_separator.new(invalid_arg).execute).to eq(nil)
+ end
+ end
+
+ shared_examples 'arg line with valid parameters' do
+ it 'returns text and date time array' do
+ freeze_time do
+ expect(timeline_text_and_datetime_separator.new(valid_arg).execute).to eq(expected_response)
+ end
+ end
+ end
+
+ describe 'execute' do
+ context 'with invalid parameters in arg line' do
+ context 'with empty arg line' do
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { '' }
+ end
+ end
+
+ context 'with invalid date' do
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { 'timeline comment | 2022-13-13 09:30' }
+ end
+
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { 'timeline comment | 2022-09/09 09:30' }
+ end
+
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { 'timeline comment | 2022-09.09 09:30' }
+ end
+ end
+
+ context 'with invalid time' do
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { 'timeline comment | 2022-11-13 29:30' }
+ end
+ end
+
+ context 'when date is invalid in arg line' do
+ let(:invalid_arg) { 'timeline comment | wrong data type' }
+
+ it 'return current date' do
+ timeline_args = timeline_text_and_datetime_separator.new(invalid_arg).execute
+
+ expect(timeline_args).to be_an_instance_of(Array)
+ expect(timeline_args.first).to eq('timeline comment')
+ expect(timeline_args.second).to match(Gitlab::QuickActions::TimelineTextAndDateTimeSeparator::DATETIME_REGEX)
+ end
+ end
+ end
+
+ context 'with valid parameters' do
+ context 'when only timeline text present in arg line' do
+ it_behaves_like 'arg line with valid parameters' do
+ let(:timeline_text) { 'timeline comment' }
+ let(:valid_arg) { timeline_text }
+ let(:date) { DateTime.current.strftime("%Y-%m-%d %H:%M:00 UTC") }
+ let(:expected_response) { [timeline_text, date] }
+ end
+ end
+
+ context 'when only timeline text and time present in arg line' do
+ it_behaves_like 'arg line with valid parameters' do
+ let(:timeline_text) { 'timeline comment' }
+ let(:date) { '09:30' }
+ let(:valid_arg) { "#{timeline_text} | #{date}" }
+ let(:parsed_date) { DateTime.parse(date) }
+ let(:expected_response) { [timeline_text, parsed_date] }
+ end
+ end
+
+ context 'when timeline text and date is present in arg line' do
+ it_behaves_like 'arg line with valid parameters' do
+ let(:timeline_text) { 'timeline comment' }
+ let(:date) { '2022-06-05 09:30' }
+ let(:valid_arg) { "#{timeline_text} | #{date}" }
+ let(:parsed_date) { DateTime.parse(date) }
+ let(:expected_response) { [timeline_text, parsed_date] }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis/boolean_spec.rb b/spec/lib/gitlab/redis/boolean_spec.rb
index 9c233ba089f..661261c79da 100644
--- a/spec/lib/gitlab/redis/boolean_spec.rb
+++ b/spec/lib/gitlab/redis/boolean_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'fast_spec_helper'
RSpec.describe Gitlab::Redis::Boolean do
subject(:redis_boolean) { described_class.new(bool) }
diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb
index 1f0ebbe107f..82ff8a26199 100644
--- a/spec/lib/gitlab/redis/cache_spec.rb
+++ b/spec/lib/gitlab/redis/cache_spec.rb
@@ -17,8 +17,8 @@ RSpec.describe Gitlab::Redis::Cache do
end
describe '.active_support_config' do
- it 'has a default ttl of 2 weeks' do
- expect(described_class.active_support_config[:expires_in]).to eq(2.weeks)
+ it 'has a default ttl of 8 hours' do
+ expect(described_class.active_support_config[:expires_in]).to eq(8.hours)
end
it 'allows configuring the TTL through an env variable' do
diff --git a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb
index 53e3d73d17e..be20e6dcdaf 100644
--- a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb
+++ b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::Redis::DuplicateJobs do
expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
expect(redis_instance.primary_store.connection[:namespace]).to be_nil
- expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0")
+ expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
expect(redis_instance.secondary_store.connection[:namespace]).to eq("resque:gitlab")
expect(redis_instance.instance_name).to eq('DuplicateJobs')
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index ef8549548d7..8b73b5e03c0 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -264,13 +264,20 @@ RSpec.describe Gitlab::Redis::MultiStore do
context 'when the command is executed within pipelined block' do
subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
+ multi_store.pipelined do |pipeline|
+ pipeline.send(name, *args)
end
end
- it 'is executed only 1 time on primary instance' do
- expect(primary_store).to receive(name).with(*args).once
+ it 'is executed only 1 time on primary and secondary instance' do
+ expect(primary_store).to receive(:pipelined).and_call_original
+ expect(secondary_store).to receive(:pipelined).and_call_original
+
+ 2.times do
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(name).with(*args).once.and_call_original
+ end
+ end
subject
end
@@ -438,14 +445,21 @@ RSpec.describe Gitlab::Redis::MultiStore do
context 'when the command is executed within pipelined block' do
subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
+ multi_store.pipelined do |pipeline|
+ pipeline.send(name, *args)
end
end
it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).once
- expect(secondary_store).to receive(name).with(*expected_args).once
+ expect(primary_store).to receive(:pipelined).and_call_original
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ end
+
+ expect(secondary_store).to receive(:pipelined).and_call_original
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ end
subject
end
@@ -781,14 +795,20 @@ RSpec.describe Gitlab::Redis::MultiStore do
context 'when the command is executed within pipelined block' do
subject do
- multi_store.pipelined do
- multi_store.incr(key)
+ multi_store.pipelined do |pipeline|
+ pipeline.incr(key)
end
end
it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).once
- expect(secondary_store).to receive(:incr).with(key).once
+ expect(primary_store).to receive(:pipelined).once.and_call_original
+ expect(secondary_store).to receive(:pipelined).once.and_call_original
+
+ 2.times do
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(:incr).with(key).once
+ end
+ end
subject
end
diff --git a/spec/lib/gitlab/redis/sidekiq_status_spec.rb b/spec/lib/gitlab/redis/sidekiq_status_spec.rb
index f641ea40efd..76d130d67f7 100644
--- a/spec/lib/gitlab/redis/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/redis/sidekiq_status_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::Redis::SidekiqStatus do
expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
- expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0")
+ expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
expect(redis_instance.instance_name).to eq('SidekiqStatus')
end
diff --git a/spec/lib/gitlab/render_timeout_spec.rb b/spec/lib/gitlab/render_timeout_spec.rb
index f322d71867b..b1386855fd5 100644
--- a/spec/lib/gitlab/render_timeout_spec.rb
+++ b/spec/lib/gitlab/render_timeout_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::RenderTimeout do
def expect_timeout(period)
diff --git a/spec/lib/gitlab/seeder_spec.rb b/spec/lib/gitlab/seeder_spec.rb
index 0ad80323085..a94ae2bca7a 100644
--- a/spec/lib/gitlab/seeder_spec.rb
+++ b/spec/lib/gitlab/seeder_spec.rb
@@ -77,44 +77,4 @@ RSpec.describe Gitlab::Seeder do
end
end
end
-
- describe ::Gitlab::Seeder::Ci::DailyBuildGroupReportResult do
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, :repository, group: group) }
- let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
-
- subject(:build_report) do
- described_class.new(project)
- end
-
- describe '#seed' do
- it 'creates daily build results for the project' do
- expect { build_report.seed }.to change {
- Ci::DailyBuildGroupReportResult.count
- }.by(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
- end
-
- it 'matches project data with last report' do
- build_report.seed
-
- report = project.daily_build_group_report_results.last
- reports_count = project.daily_build_group_report_results.count
-
- expect(build.group_name).to eq(report.group_name)
- expect(pipeline.source_ref_path).to eq(report.ref_path)
- expect(pipeline.default_branch?).to eq(report.default_branch)
- expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
- end
-
- it 'does not raise error on RecordNotUnique' do
- build_report.seed
- build_report.seed
-
- reports_count = project.daily_build_group_report_results.count
-
- expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
- end
- end
- end
end
diff --git a/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb b/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb
new file mode 100644
index 00000000000..4b41122d23c
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::Ci::DailyBuildGroupReportResult do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ subject(:build_report) do
+ described_class.new(project)
+ end
+
+ describe '#seed' do
+ it 'creates daily build results for the project' do
+ expect { build_report.seed }.to change {
+ Ci::DailyBuildGroupReportResult.count
+ }.by(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
+ end
+
+ it 'matches project data with last report' do
+ build_report.seed
+
+ report = project.daily_build_group_report_results.last
+ reports_count = project.daily_build_group_report_results.count
+
+ expect(build.group_name).to eq(report.group_name)
+ expect(pipeline.source_ref_path).to eq(report.ref_path)
+ expect(pipeline.default_branch?).to eq(report.default_branch)
+ expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
+ end
+
+ it 'does not raise error on RecordNotUnique' do
+ build_report.seed
+ build_report.seed
+
+ reports_count = project.daily_build_group_report_results.count
+
+ expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/service_desk_email_spec.rb b/spec/lib/gitlab/service_desk_email_spec.rb
index 9847496e361..6667b61c02b 100644
--- a/spec/lib/gitlab/service_desk_email_spec.rb
+++ b/spec/lib/gitlab/service_desk_email_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::ServiceDeskEmail do
describe '.enabled?' do
diff --git a/spec/lib/gitlab/session_spec.rb b/spec/lib/gitlab/session_spec.rb
index 67ad59f956d..171288da1d5 100644
--- a/spec/lib/gitlab/session_spec.rb
+++ b/spec/lib/gitlab/session_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Session do
it 'uses the current thread as a data store' do
diff --git a/spec/lib/gitlab/setup_helper/workhorse_spec.rb b/spec/lib/gitlab/setup_helper/workhorse_spec.rb
index 18cb266bf4e..726b73a9dfe 100644
--- a/spec/lib/gitlab/setup_helper/workhorse_spec.rb
+++ b/spec/lib/gitlab/setup_helper/workhorse_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::SetupHelper::Workhorse do
describe '.make' do
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 891b3639709..785429aa3b0 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -9,9 +9,13 @@ RSpec.describe Gitlab::Shell do
let(:repository) { project.repository }
let(:gitlab_shell) { described_class.new }
+ before do
+ described_class.instance_variable_set(:@secret_token, nil)
+ end
+
it { is_expected.to respond_to :remove_repository }
- describe 'memoized secret_token' do
+ describe '.secret_token' do
let(:secret_file) { 'tmp/tests/.secret_shell_test' }
let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }
@@ -19,7 +23,6 @@ RSpec.describe Gitlab::Shell do
allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
FileUtils.mkdir('tmp/tests/shell-secret-test')
- described_class.ensure_secret_token!
end
after do
@@ -27,13 +30,47 @@ RSpec.describe Gitlab::Shell do
FileUtils.rm_rf(secret_file)
end
- it 'creates and links the secret token file' do
- secret_token = described_class.secret_token
+ shared_examples 'creates and links the secret token file' do
+ it 'creates and links the secret token file' do
+ secret_token = described_class.secret_token
+
+ expect(File.exist?(secret_file)).to be(true)
+ expect(File.read(secret_file).chomp).to eq(secret_token)
+ expect(File.symlink?(link_file)).to be(true)
+ expect(File.readlink(link_file)).to eq(secret_file)
+ end
+ end
+
+ describe 'memoized secret_token' do
+ before do
+ described_class.ensure_secret_token!
+ end
+
+ it_behaves_like 'creates and links the secret token file'
+ end
+
+ context 'when link_file is a broken symbolic link' do
+ before do
+ File.symlink('tmp/tests/non_existing_file', link_file)
+ described_class.ensure_secret_token!
+ end
+
+ it_behaves_like 'creates and links the secret token file'
+ end
+
+ context 'when secret_file exists' do
+ let(:secret_token) { 'secret-token' }
- expect(File.exist?(secret_file)).to be(true)
- expect(File.read(secret_file).chomp).to eq(secret_token)
- expect(File.symlink?(link_file)).to be(true)
- expect(File.readlink(link_file)).to eq(secret_file)
+ before do
+ File.write(secret_file, 'secret-token')
+ described_class.ensure_secret_token!
+ end
+
+ it_behaves_like 'creates and links the secret token file'
+
+ it 'reads the token from the existing file' do
+ expect(described_class.secret_token).to eq(secret_token)
+ end
end
end
diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
index 635f572daef..dff04a2e509 100644
--- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
@@ -326,7 +326,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
class: described_class.to_s,
signal: signal,
pid: pid,
- message: "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})")
+ message: "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})")
expect(Process).to receive(:kill).with(signal, pid).ordered
subject
@@ -340,7 +340,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
class: described_class.to_s,
signal: signal,
pid: pid,
- message: "sending Sidekiq worker PGRP-#{pid} #{signal} (#{explanation})")
+ message: "sending Sidekiq worker PGRP-#{pid} #{signal} (#{explanation})")
expect(Process).to receive(:kill).with(signal, 0).ordered
subject
diff --git a/spec/lib/gitlab/sidekiq_death_handler_spec.rb b/spec/lib/gitlab/sidekiq_death_handler_spec.rb
index e3f9f8277a0..434642bf3ef 100644
--- a/spec/lib/gitlab/sidekiq_death_handler_spec.rb
+++ b/spec/lib/gitlab/sidekiq_death_handler_spec.rb
@@ -24,8 +24,8 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do
expect(described_class.counter)
.to receive(:increment)
.with({ queue: 'test_queue', worker: 'TestWorker',
- urgency: 'low', external_dependencies: 'yes',
- feature_category: 'users', boundary: 'cpu' })
+ urgency: 'low', external_dependencies: 'yes',
+ feature_category: 'users', boundary: 'cpu' })
described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
end
@@ -40,8 +40,8 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do
expect(described_class.counter)
.to receive(:increment)
.with({ queue: 'test_queue', worker: 'TestWorker',
- urgency: '', external_dependencies: 'no',
- feature_category: '', boundary: '' })
+ urgency: '', external_dependencies: 'no',
+ feature_category: '', boundary: '' })
described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
index 09548d21106..cc730e203f6 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
@@ -41,10 +41,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r
describe '#call' do
it 'removes the stored job from redis before execution' do
bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] }
- job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication')
+ job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'default')
expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob)
- .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication')
+ .to receive(:new).with(a_hash_including(bare_job), 'default')
.and_return(job_definition).twice # once in client middleware
expect(job_definition).to receive(:delete!).ordered.and_call_original
@@ -60,10 +60,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r
it 'removes the stored job from redis after execution' do
bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] }
- job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication')
+ job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'default')
expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob)
- .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication')
+ .to receive(:new).with(a_hash_including(bare_job), 'default')
.and_return(job_definition).twice # once in client middleware
expect(TestDeduplicationWorker).to receive(:work).ordered.and_call_original
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index d6d24ea3a24..52b50a143fc 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -22,39 +22,39 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
expect(completion_seconds_metric)
.to receive(:get).with({ queue: 'merge',
- worker: 'MergeWorker',
- urgency: 'high',
- external_dependencies: 'no',
- feature_category: 'source_code_management',
- boundary: '',
- job_status: 'done' })
+ worker: 'MergeWorker',
+ urgency: 'high',
+ external_dependencies: 'no',
+ feature_category: 'source_code_management',
+ boundary: '',
+ job_status: 'done' })
expect(completion_seconds_metric)
.to receive(:get).with({ queue: 'merge',
- worker: 'MergeWorker',
- urgency: 'high',
- external_dependencies: 'no',
- feature_category: 'source_code_management',
- boundary: '',
- job_status: 'fail' })
+ worker: 'MergeWorker',
+ urgency: 'high',
+ external_dependencies: 'no',
+ feature_category: 'source_code_management',
+ boundary: '',
+ job_status: 'fail' })
expect(completion_seconds_metric)
.to receive(:get).with({ queue: 'default',
- worker: 'Ci::BuildFinishedWorker',
- urgency: 'high',
- external_dependencies: 'no',
- feature_category: 'continuous_integration',
- boundary: 'cpu',
- job_status: 'done' })
+ worker: 'Ci::BuildFinishedWorker',
+ urgency: 'high',
+ external_dependencies: 'no',
+ feature_category: 'continuous_integration',
+ boundary: 'cpu',
+ job_status: 'done' })
expect(completion_seconds_metric)
.to receive(:get).with({ queue: 'default',
- worker: 'Ci::BuildFinishedWorker',
- urgency: 'high',
- external_dependencies: 'no',
- feature_category: 'continuous_integration',
- boundary: 'cpu',
- job_status: 'fail' })
+ worker: 'Ci::BuildFinishedWorker',
+ urgency: 'high',
+ external_dependencies: 'no',
+ feature_category: 'continuous_integration',
+ boundary: 'cpu',
+ job_status: 'fail' })
described_class.initialize_process_metrics
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb
index 91b8ef97ab4..12430313141 100644
--- a/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Server, :clean_gitlab_redis_queues do
diff --git a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb
index d4391d3023a..a576cf3e2ab 100644
--- a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb
+++ b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 3, migrated: 0)
expect(set_after.length).to eq(3)
- expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects',
+ expect(set_after.map(&:first)).to all(include('queue' => 'default',
'class' => 'AuthorizedProjectsWorker'))
end
end
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
if item['class'] == 'AuthorizedProjectsWorker'
expect(item).to include('queue' => 'new_queue', 'args' => [i])
else
- expect(item).to include('queue' => 'post_receive', 'args' => [i])
+ expect(item).to include('queue' => 'default', 'args' => [i])
end
expect(score).to be_within(schedule_jitter).of(i.succ.hours.from_now.to_i)
@@ -116,7 +116,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 4, migrated: 0)
expect(set_after.length).to eq(3)
- expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects'))
+ expect(set_after.map(&:first)).to all(include('queue' => 'default'))
end
end
@@ -138,7 +138,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 4, migrated: 1)
expect(set_after.group_by { |job| job.first['queue'] }.transform_values(&:count))
- .to eq('authorized_projects' => 6, 'new_queue' => 1)
+ .to eq('default' => 6, 'new_queue' => 1)
end
it 'iterates through the entire set of jobs' do
diff --git a/spec/lib/gitlab/sidekiq_queue_spec.rb b/spec/lib/gitlab/sidekiq_queue_spec.rb
index 5e91282612e..93632848788 100644
--- a/spec/lib/gitlab/sidekiq_queue_spec.rb
+++ b/spec/lib/gitlab/sidekiq_queue_spec.rb
@@ -4,15 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do
around do |example|
- Sidekiq::Queue.new('default').clear
+ Sidekiq::Queue.new('foobar').clear
Sidekiq::Testing.disable!(&example)
- Sidekiq::Queue.new('default').clear
+ Sidekiq::Queue.new('foobar').clear
end
def add_job(args, user:, klass: 'AuthorizedProjectsWorker')
Sidekiq::Client.push(
'class' => klass,
- 'queue' => 'default',
+ 'queue' => 'foobar',
'args' => args,
'meta.user' => user.username
)
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do
describe '#drop_jobs!' do
shared_examples 'queue processing' do
- let(:sidekiq_queue) { described_class.new('default') }
+ let(:sidekiq_queue) { described_class.new('foobar') }
let_it_be(:sidekiq_queue_user) { create(:user) }
before do
@@ -80,7 +80,7 @@ RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do
it 'raises NoMetadataError' do
add_job([1], user: create(:user))
- expect { described_class.new('default').drop_jobs!({ username: 'sidekiq_queue_user' }, timeout: 1) }
+ expect { described_class.new('foobar').drop_jobs!({ username: 'sidekiq_queue_user' }, timeout: 1) }
.to raise_error(described_class::NoMetadataError)
end
end
diff --git a/spec/lib/gitlab/sidekiq_signals_spec.rb b/spec/lib/gitlab/sidekiq_signals_spec.rb
index 2f751839f6a..734b9e79088 100644
--- a/spec/lib/gitlab/sidekiq_signals_spec.rb
+++ b/spec/lib/gitlab/sidekiq_signals_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::SidekiqSignals do
describe '.install' do
diff --git a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb
index 5a0c4cbd1b5..c0fd88eab1b 100644
--- a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::SidekiqStatus::ServerMiddleware do
describe '#call' do
diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb
index afafd04d87d..bdbba04e0c0 100644
--- a/spec/lib/gitlab/sidekiq_versioning_spec.rb
+++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb
@@ -2,30 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::SidekiqVersioning, :redis do
- let(:foo_worker) do
- Class.new do
- def self.name
- 'FooWorker'
- end
-
- include ApplicationWorker
- end
- end
-
- let(:bar_worker) do
- Class.new do
- def self.name
- 'BarWorker'
- end
-
- include ApplicationWorker
- end
- end
-
+RSpec.describe Gitlab::SidekiqVersioning, :clean_gitlab_redis_queues do
before do
- allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker])
- allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue])
+ allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return(%w[foo bar])
end
describe '.install!' do
diff --git a/spec/lib/gitlab/slug/environment_spec.rb b/spec/lib/gitlab/slug/environment_spec.rb
index f516322b937..e8f0fba27b2 100644
--- a/spec/lib/gitlab/slug/environment_spec.rb
+++ b/spec/lib/gitlab/slug/environment_spec.rb
@@ -1,27 +1,27 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Slug::Environment do
describe '#generate' do
{
"staging-12345678901234567" => "staging-123456789-q517sa",
"9-staging-123456789012345" => "env-9-staging-123-q517sa",
- "staging-1234567890123456" => "staging-1234567890123456",
+ "staging-1234567890123456" => "staging-1234567890123456",
"staging-1234567890123456-" => "staging-123456789-q517sa",
- "production" => "production",
- "PRODUCTION" => "production-q517sa",
- "review/1-foo" => "review-1-foo-q517sa",
- "1-foo" => "env-1-foo-q517sa",
- "1/foo" => "env-1-foo-q517sa",
- "foo-" => "foo",
- "foo--bar" => "foo-bar-q517sa",
- "foo**bar" => "foo-bar-q517sa",
- "*-foo" => "env-foo-q517sa",
- "staging-12345678-" => "staging-12345678",
+ "production" => "production",
+ "PRODUCTION" => "production-q517sa",
+ "review/1-foo" => "review-1-foo-q517sa",
+ "1-foo" => "env-1-foo-q517sa",
+ "1/foo" => "env-1-foo-q517sa",
+ "foo-" => "foo",
+ "foo--bar" => "foo-bar-q517sa",
+ "foo**bar" => "foo-bar-q517sa",
+ "*-foo" => "env-foo-q517sa",
+ "staging-12345678-" => "staging-12345678",
"staging-12345678-01234567" => "staging-12345678-q517sa",
- "" => "env-q517sa",
- nil => "env-q517sa"
+ "" => "env-q517sa",
+ nil => "env-q517sa"
}.each do |name, matcher|
before do
# ('a' * 64).to_i(16).to_s(36).last(6) gives 'q517sa'
diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb
index 956ed2a976f..2fe978125c4 100644
--- a/spec/lib/gitlab/spamcheck/client_spec.rb
+++ b/spec/lib/gitlab/spamcheck/client_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
let_it_be(:issue) { create(:issue, description: 'Test issue description') }
+ let_it_be(:snippet) { create(:personal_snippet, :public, description: 'Test issue description') }
let(:response) do
verdict = ::Spamcheck::SpamVerdict.new
@@ -26,7 +27,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
verdict
end
- subject { described_class.new.issue_spam?(spam_issue: issue, user: user) }
+ subject { described_class.new.spam?(spammable: issue, user: user) }
before do
stub_application_setting(spam_check_endpoint_url: endpoint)
@@ -56,10 +57,11 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
- describe '#issue_spam?' do
+ shared_examples 'check for spam' do
before do
allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance|
allow(instance).to receive(:check_for_spam_issue).and_return(response)
+ allow(instance).to receive(:check_for_spam_snippet).and_return(response)
end
end
@@ -89,12 +91,26 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
- describe "#build_issue_protobuf", :aggregate_failures do
- it 'builds the expected protobuf object' do
+ describe "#spam?", :aggregate_failures do
+ describe 'issue' do
+ subject { described_class.new.spam?(spammable: issue, user: user) }
+
+ it_behaves_like "check for spam"
+ end
+
+ describe 'snippet' do
+ subject { described_class.new.spam?(spammable: snippet, user: user, extra_features: { files: [{ path: "file.rb" }] }) }
+
+ it_behaves_like "check for spam"
+ end
+ end
+
+ describe "#build_protobuf", :aggregate_failures do
+ it 'builds the expected issue protobuf object' do
cxt = { action: :create }
- issue_pb = described_class.new.send(:build_issue_protobuf,
- issue: issue, user: user,
- context: cxt)
+ issue_pb, _ = described_class.new.send(:build_protobuf,
+ spammable: issue, user: user,
+ context: cxt, extra_features: {})
expect(issue_pb.title).to eq issue.title
expect(issue_pb.description).to eq issue.description
expect(issue_pb.user_in_project).to be false
@@ -104,6 +120,22 @@ RSpec.describe Gitlab::Spamcheck::Client do
expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(issue_pb.user.username).to eq user.username
end
+
+ it 'builds the expected snippet protobuf object' do
+ cxt = { action: :create }
+ snippet_pb, _ = described_class.new.send(:build_protobuf,
+ spammable: snippet, user: user,
+ context: cxt, extra_features: { files: [{ path: 'first.rb' }, { path: 'second.rb' }] })
+ expect(snippet_pb.title).to eq snippet.title
+ expect(snippet_pb.description).to eq snippet.description
+ expect(snippet_pb.created_at).to eq timestamp_to_protobuf_timestamp(snippet.created_at)
+ expect(snippet_pb.updated_at).to eq timestamp_to_protobuf_timestamp(snippet.updated_at)
+ expect(snippet_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
+ expect(snippet_pb.user.username).to eq user.username
+ expect(snippet_pb.user.username).to eq user.username
+ expect(snippet_pb.files.first.path).to eq 'first.rb'
+ expect(snippet_pb.files.last.path).to eq 'second.rb'
+ end
end
describe '#build_user_protobuf', :aggregate_failures do
@@ -143,6 +175,19 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
+ describe "#get_spammable_mappings", :aggregate_failures do
+ it 'is an expected spammable' do
+ protobuf_class, _ = described_class.new.send(:get_spammable_mappings, issue)
+ expect(protobuf_class).to eq ::Spamcheck::Issue
+ end
+
+ it 'is an unexpected spammable' do
+ expect { described_class.new.send(:get_spammable_mappings, 'spam') }.to raise_error(
+ ArgumentError, 'Not a spammable type: String'
+ )
+ end
+ end
+
private
def timestamp_to_protobuf_timestamp(timestamp)
diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb
index 8f17bf64005..9f477998be2 100644
--- a/spec/lib/gitlab/string_placeholder_replacer_spec.rb
+++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::StringPlaceholderReplacer do
describe '.render_url' do
diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb
index 6f63c8e2df4..2ababd6a938 100644
--- a/spec/lib/gitlab/string_range_marker_spec.rb
+++ b/spec/lib/gitlab/string_range_marker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::StringRangeMarker do
describe '#mark' do
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
index 0cbe44eacf4..393bfea7c6b 100644
--- a/spec/lib/gitlab/string_regex_marker_spec.rb
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::StringRegexMarker do
describe '#mark' do
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index 098a58bff83..f93eb6f96cc 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -53,12 +53,13 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
it { is_expected.to match(link_match) }
end
- context 'url methods' do
+ describe 'class methods' do
where(:method_name, :result) do
:default_subscriptions_url | staging_customers_url
:payment_form_url | "#{staging_customers_url}/payment_forms/cc_validation"
:payment_validation_form_id | 'payment_method_validation'
:registration_validation_form_url | "#{staging_customers_url}/payment_forms/cc_registration_validation"
+ :registration_validation_form_id | 'cc_registration_validation'
:subscriptions_graphql_url | "#{staging_customers_url}/graphql"
:subscriptions_more_minutes_url | "#{staging_customers_url}/buy_pipeline_minutes"
:subscriptions_more_storage_url | "#{staging_customers_url}/buy_storage"
@@ -108,4 +109,16 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
is_expected.to eq(url)
end
end
+
+ describe 'constants' do
+ where(:constant_name, :result) do
+ 'REGISTRATION_VALIDATION_FORM_ID' | 'cc_registration_validation'
+ end
+
+ with_them do
+ subject { "#{described_class}::#{constant_name}".constantize }
+
+ it { is_expected.to eq(result) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb
index 12149576de0..5f9960265ec 100644
--- a/spec/lib/gitlab/tcp_checker_spec.rb
+++ b/spec/lib/gitlab/tcp_checker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::TcpChecker, :permit_dns do
before do
diff --git a/spec/lib/gitlab/tracking/incident_management_spec.rb b/spec/lib/gitlab/tracking/incident_management_spec.rb
index ef7816aa0db..c27e2548526 100644
--- a/spec/lib/gitlab/tracking/incident_management_spec.rb
+++ b/spec/lib/gitlab/tracking/incident_management_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Tracking::IncidentManagement do
describe '.track_from_params' do
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 028c985f3b3..e11175c776d 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -132,9 +132,36 @@ RSpec.describe Gitlab::Tracking do
expect(args[:context].last).to eq(other_context)
end
- described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
- context: [other_context], project: project, user: user, namespace: namespace,
- extra_key_1: 'extra value 1', extra_key_2: 'extra value 2')
+ described_class.event('category', 'action',
+ label: 'label',
+ property: 'property',
+ value: 1.5,
+ context: [other_context],
+ project: project,
+ user: user,
+ namespace: namespace,
+ extra_key_1: 'extra value 1',
+ extra_key_2: 'extra value 2')
+ end
+ end
+
+ context 'when the action is not passed in as a string' do
+ it 'allows symbols' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ described_class.event('category', :some_action)
+ end
+
+ it 'allows nil' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ described_class.event('category', nil)
+ end
+
+ it 'allows integers' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ described_class.event('category', 1)
end
end
@@ -197,8 +224,15 @@ RSpec.describe Gitlab::Tracking do
expect(args[:extra_key_1]).to eq('extra value 1')
end
- described_class.definition('filename', category: nil, action: nil, label: 'label', property: '...',
- project: project, user: user, namespace: namespace, extra_key_1: 'extra value 1')
+ described_class.definition('filename',
+ category: nil,
+ action: nil,
+ label: 'label',
+ property: '...',
+ project: project,
+ user: user,
+ namespace: namespace,
+ extra_key_1: 'extra value 1')
end
end
diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb
index f45005fcc9b..42cc15a9033 100644
--- a/spec/lib/gitlab/tree_summary_spec.rb
+++ b/spec/lib/gitlab/tree_summary_spec.rb
@@ -25,6 +25,14 @@ RSpec.describe Gitlab::TreeSummary do
it 'defaults limit to 25' do
expect(summary.limit).to eq(25)
end
+
+ context 'when offset is larger than the maximum' do
+ let(:offset) { described_class::MAX_OFFSET + 1 }
+
+ it 'sets offset to the maximum' do
+ expect(subject.offset).to eq(described_class::MAX_OFFSET)
+ end
+ end
end
describe '#summarize' do
@@ -45,6 +53,14 @@ RSpec.describe Gitlab::TreeSummary do
end
end
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it 'returns an empty array' do
+ expect(entries).to eq([])
+ end
+ end
+
context 'with caching', :use_clean_rails_memory_store_caching do
subject { Rails.cache.fetch(key) }
diff --git a/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb b/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb
index ece0a018d53..2405b6769b7 100644
--- a/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb
+++ b/spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::UrlBlockers::DomainAllowlistEntry do
let(:domain) { 'www.example.com' }
diff --git a/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb b/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb
index 110a6c17adb..8dcb402dfb2 100644
--- a/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb
+++ b/spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::UrlBlockers::IpAllowlistEntry do
let(:ipv4) { IPAddr.new('192.168.1.1') }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb
index b85d5a3ebf9..ce15d44b1e1 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb
@@ -5,15 +5,17 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitiesMetric do
let_it_be(:user) { create(:user) }
let_it_be(:bulk_import_projects) do
- create_list(:bulk_import_entity, 3, source_type: 'project_entity', created_at: 3.weeks.ago)
+ create_list(:bulk_import_entity, 2, source_type: 'project_entity', created_at: 3.weeks.ago, status: 2)
+ create(:bulk_import_entity, source_type: 'project_entity', created_at: 3.weeks.ago, status: 0)
end
let_it_be(:bulk_import_groups) do
- create_list(:bulk_import_entity, 3, source_type: 'group_entity', created_at: 3.weeks.ago)
+ create_list(:bulk_import_entity, 2, source_type: 'group_entity', created_at: 3.weeks.ago, status: 2)
+ create(:bulk_import_entity, source_type: 'group_entity', created_at: 3.weeks.ago, status: 0)
end
let_it_be(:old_bulk_import_project) do
- create(:bulk_import_entity, source_type: 'project_entity', created_at: 2.months.ago)
+ create(:bulk_import_entity, source_type: 'project_entity', created_at: 2.months.ago, status: 2)
end
context 'with no source_type' do
@@ -103,4 +105,62 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
options: { source_type: 'group_entity' }
end
end
+
+ context 'with entity status' do
+ context 'with all time frame' do
+ let(:expected_value) { 5 }
+ let(:expected_query) do
+ "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\
+ " WHERE \"bulk_import_entities\".\"status\" = 2"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: 'all',
+ options: { status: 2 }
+ end
+
+ context 'for 28d time frame' do
+ let(:expected_value) { 4 }
+ let(:start) { 30.days.ago.to_s(:db) }
+ let(:finish) { 2.days.ago.to_s(:db) }
+ let(:expected_query) do
+ "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\
+ " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"\
+ " AND \"bulk_import_entities\".\"status\" = 2"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: '28d',
+ options: { status: 2 }
+ end
+ end
+
+ context 'with entity status and source_type' do
+ context 'with all time frame' do
+ let(:expected_value) { 3 }
+ let(:expected_query) do
+ "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\
+ " WHERE \"bulk_import_entities\".\"source_type\" = 1 AND \"bulk_import_entities\".\"status\" = 2"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: 'all',
+ options: { status: 2, source_type: 'project_entity' }
+ end
+
+ context 'for 28d time frame' do
+ let(:expected_value) { 2 }
+ let(:start) { 30.days.ago.to_s(:db) }
+ let(:finish) { 2.days.ago.to_s(:db) }
+ let(:expected_query) do
+ "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\
+ " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"\
+ " AND \"bulk_import_entities\".\"source_type\" = 1 AND \"bulk_import_entities\".\"status\" = 2"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: '28d',
+ options: { status: 2, source_type: 'project_entity' }
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb
new file mode 100644
index 00000000000..2f49c427bd0
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUserAuthMetric do
+ context 'with all time frame' do
+ let(:expected_value) { 2 }
+
+ before do
+ user = create(:user)
+ user2 = create(:user)
+ create(:authentication_event, user: user, provider: :ldapmain, result: :success)
+ create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success)
+ create(:authentication_event, user: user2, provider: :group_saml, result: :success)
+ create(:authentication_event, user: user2, provider: :group_saml, result: :success)
+ create(:authentication_event, user: user, provider: :group_saml, result: :failed)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+
+ context 'with 28d time frame' do
+ let(:expected_value) { 1 }
+
+ before do
+ user = create(:user)
+ user2 = create(:user)
+
+ create(:authentication_event, created_at: 1.year.ago, user: user, provider: :ldapmain, result: :success)
+ create(:authentication_event, created_at: 1.week.ago, user: user2, provider: :ldapsecondary, result: :success)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
index 831f775ec9a..80ae5c6fd21 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
@@ -11,18 +11,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git
let(:expected_value) { 4 }
- it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', counter_class: 'SourceCodeCounter' } }
+ it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', prefix: 'source_code' } }
it 'raises an exception if event option is not present' do
- expect { described_class.new(counter_class: 'SourceCodeCounter') }.to raise_error(ArgumentError)
+ expect { described_class.new(prefix: 'source_code') }.to raise_error(ArgumentError)
end
- it 'raises an exception if counter_class option is not present' do
+ it 'raises an exception if prefix option is not present' do
expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError)
end
describe 'children classes' do
- let(:options) { { event: 'pushes', counter_class: 'SourceCodeCounter' } }
+ let(:options) { { event: 'pushes', prefix: 'source_code' } }
context 'availability not defined' do
subject { Class.new(described_class).new(time_frame: nil, options: options) }
@@ -44,4 +44,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git
end
end
end
+
+ context "with usage prefix disabled" do
+ let(:expected_value) { 3 }
+
+ before do
+ 3.times do
+ Gitlab::UsageDataCounters::WebIdeCounter.increment_merge_requests_count
+ end
+ end
+
+ it_behaves_like 'a correct instrumented metric value', {
+ options: { event: 'merge_requests_count', prefix: 'web_ide', include_usage_prefix: false }
+ }
+ end
end
diff --git a/spec/lib/gitlab/usage_data/topology_spec.rb b/spec/lib/gitlab/usage_data/topology_spec.rb
index 737580e3493..dfdf8eaabe8 100644
--- a/spec/lib/gitlab/usage_data/topology_spec.rb
+++ b/spec/lib/gitlab/usage_data/topology_spec.rb
@@ -187,7 +187,7 @@ RSpec.describe Gitlab::UsageData::Topology do
[
{
'metric' => { 'instance' => 'localhost:9100' },
- 'value' => [1000, '512']
+ 'value' => [1000, '512']
}
]
end
@@ -196,7 +196,7 @@ RSpec.describe Gitlab::UsageData::Topology do
[
{
'metric' => { 'instance' => 'localhost:9100' },
- 'value' => [1000, '0.35']
+ 'value' => [1000, '0.35']
}
]
end
@@ -224,23 +224,23 @@ RSpec.describe Gitlab::UsageData::Topology do
[
{
'metric' => { 'instance' => 'localhost:8080', 'job' => 'gitlab-rails' },
- 'value' => [1000, '10']
+ 'value' => [1000, '10']
},
{
'metric' => { 'instance' => '127.0.0.1:8090', 'job' => 'gitlab-sidekiq' },
- 'value' => [1000, '11']
+ 'value' => [1000, '11']
},
{
'metric' => { 'instance' => '0.0.0.0:9090', 'job' => 'prometheus' },
- 'value' => [1000, '12']
+ 'value' => [1000, '12']
},
{
'metric' => { 'instance' => '[::1]:1234', 'job' => 'redis' },
- 'value' => [1000, '13']
+ 'value' => [1000, '13']
},
{
'metric' => { 'instance' => '[::]:1234', 'job' => 'postgres' },
- 'value' => [1000, '14']
+ 'value' => [1000, '14']
}
]
end
@@ -640,7 +640,7 @@ RSpec.describe Gitlab::UsageData::Topology do
.and_return(result || [
{
'metric' => { 'instance' => 'instance1:8080', 'job' => 'gitlab-rails' },
- 'value' => [1000, '300']
+ 'value' => [1000, '300']
},
{
'metric' => { 'instance' => 'instance1:8090', 'job' => 'gitlab-sidekiq' },
diff --git a/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb
index 4a31191d75f..9cecaa01885 100644
--- a/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::UsageDataCounters::BaseCounter do
describe '.fetch_supported_event' do
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index e0b334cb5af..3fb2532521a 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -107,10 +107,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'quickactions',
'pipeline_authoring',
'epics_usage',
- 'epic_boards_usage',
'secure',
'importer',
- 'network_policies',
'geo',
'growth',
'work_items',
diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
index 84a6f338282..032a5e78385 100644
--- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
let(:time) { Time.zone.now }
context 'for Issue title edit actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_TITLE_CHANGED }
def track_action(params)
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue description edit actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_DESCRIPTION_CHANGED }
def track_action(params)
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue assignee edit actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_ASSIGNEE_CHANGED }
def track_action(params)
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue make confidential actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_MADE_CONFIDENTIAL }
def track_action(params)
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue make visible actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_MADE_VISIBLE }
def track_action(params)
@@ -65,7 +65,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue created actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_CREATED }
def track_action(params)
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue closed actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_CLOSED }
def track_action(params)
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue reopened actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_REOPENED }
def track_action(params)
@@ -95,7 +95,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue label changed actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_LABEL_CHANGED }
def track_action(params)
@@ -104,8 +104,18 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
end
+ context 'for Issue label milestone actions' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
+ let(:action) { described_class::ISSUE_MILESTONE_CHANGED }
+
+ def track_action(params)
+ described_class.track_issue_milestone_changed_action(**params)
+ end
+ end
+ end
+
context 'for Issue cross-referenced actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_CROSS_REFERENCED }
def track_action(params)
@@ -115,7 +125,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue moved actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_MOVED }
def track_action(params)
@@ -135,7 +145,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue relate actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_RELATED }
def track_action(params)
@@ -145,7 +155,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue unrelate actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_UNRELATED }
def track_action(params)
@@ -155,7 +165,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue marked as duplicate actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE }
def track_action(params)
@@ -165,7 +175,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue locked actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_LOCKED }
def track_action(params)
@@ -175,7 +185,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue unlocked actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_UNLOCKED }
def track_action(params)
@@ -185,7 +195,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue designs added actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_DESIGNS_ADDED }
def track_action(params)
@@ -195,7 +205,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue designs modified actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_DESIGNS_MODIFIED }
def track_action(params)
@@ -205,7 +215,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue designs removed actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_DESIGNS_REMOVED }
def track_action(params)
@@ -215,7 +225,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue due date changed actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_DUE_DATE_CHANGED }
def track_action(params)
@@ -225,7 +235,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue time estimate changed actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_TIME_ESTIMATE_CHANGED }
def track_action(params)
@@ -235,7 +245,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue time spent changed actions' do
- it_behaves_like 'a daily tracked issuable event' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
let(:action) { described_class::ISSUE_TIME_SPENT_CHANGED }
def track_action(params)
@@ -275,15 +285,15 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
it 'can return the count of actions per user deduplicated', :aggregate_failures do
- described_class.track_issue_title_changed_action(author: user1)
- described_class.track_issue_description_changed_action(author: user1)
- described_class.track_issue_assignee_changed_action(author: user1)
+ described_class.track_issue_title_changed_action(author: user1, project: project)
+ described_class.track_issue_description_changed_action(author: user1, project: project)
+ described_class.track_issue_assignee_changed_action(author: user1, project: project)
travel_to(2.days.ago) do
- described_class.track_issue_title_changed_action(author: user2)
- described_class.track_issue_title_changed_action(author: user3)
- described_class.track_issue_description_changed_action(author: user3)
- described_class.track_issue_assignee_changed_action(author: user3)
+ described_class.track_issue_title_changed_action(author: user2, project: project)
+ described_class.track_issue_title_changed_action(author: user3, project: project)
+ described_class.track_issue_description_changed_action(author: user3, project: project)
+ described_class.track_issue_assignee_changed_action(author: user3, project: project)
end
events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(described_class::ISSUE_CATEGORY)
diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
index 3f44cfdcf27..74e63d219bd 100644
--- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
@@ -100,9 +100,9 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
subject
expect_snowplow_event(
- category: 'merge_requests',
+ category: 'merge_requests',
action: 'i_code_review_user_approve_mr',
- namespace: target_project.namespace,
+ namespace: target_project.namespace,
user: user,
project: target_project
)
diff --git a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb
index 7e8f0172e06..687f8c2cd41 100644
--- a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::UsageDataCounters::NoteCounter, :clean_gitlab_redis_share
let(:expected_totals) do
{ snippet_comment: 3,
merge_request_comment: 4,
- commit_comment: 5 }
+ commit_comment: 5 }
end
before do
diff --git a/spec/lib/gitlab/usage_data_counters_spec.rb b/spec/lib/gitlab/usage_data_counters_spec.rb
index 0696b375eb5..040b5deca54 100644
--- a/spec/lib/gitlab/usage_data_counters_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::UsageDataCounters do
describe '.usage_data_counters' do
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index 485f2131d87..ed0eabf1b4d 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageDataMetrics do
allow_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter|
allow(batch_counter).to receive(:transaction_open?).and_return(false)
end
+
+ allow_next_instance_of(Gitlab::Database::BatchAverageCounter) do |instance|
+ allow(instance).to receive(:transaction_open?).and_return(false)
+ end
end
context 'with instrumentation_class' do
@@ -33,6 +37,10 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject[:usage_activity_by_stage][:plan]).to include(:issues)
end
+ it 'includes usage_activity_by_stage metrics' do
+ expect(subject[:usage_activity_by_stage][:manage]).to include(:count_user_auth)
+ end
+
it 'includes usage_activity_by_stage_monthly keys' do
expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues)
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 692b6483149..46ed4b57d3a 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -215,14 +215,28 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
groups: 2,
users_created: 10,
omniauth_providers: ['google_oauth2'],
- user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
+ user_auth_by_provider: {
+ 'group_saml' => 2,
+ 'ldap' => 4,
+ 'standard' => 0,
+ 'two-factor' => 0,
+ 'two-factor-via-u2f-device' => 0,
+ "two-factor-via-webauthn-device" => 0
+ }
)
expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
events: be_within(error_rate).percent_of(2),
groups: 1,
users_created: 6,
omniauth_providers: ['google_oauth2'],
- user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
+ user_auth_by_provider: {
+ 'group_saml' => 1,
+ 'ldap' => 2,
+ 'standard' => 0,
+ 'two-factor' => 0,
+ 'two-factor-via-u2f-device' => 0,
+ "two-factor-via-webauthn-device" => 0
+ }
)
end
@@ -583,10 +597,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
it 'gathers object store usage correctly' do
expect(subject[:object_store]).to eq(
{ artifacts: { enabled: true, object_store: { enabled: true, direct_upload: true, background_upload: false, provider: "AWS" } },
- external_diffs: { enabled: false },
- lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
- uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
- packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }
+ external_diffs: { enabled: false },
+ lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
+ uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
+ packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }
)
end
@@ -749,9 +763,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
it { is_expected.to include(:kubernetes_agent_gitops_sync) }
it { is_expected.to include(:kubernetes_agent_k8s_api_proxy_request) }
- it { is_expected.to include(:package_events_i_package_pull_package) }
- it { is_expected.to include(:package_events_i_package_delete_package_by_user) }
- it { is_expected.to include(:package_events_i_package_conan_push_package) }
end
describe '.usage_data_counters' do
diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb
index 7595fb2c1b0..6b0be4590f1 100644
--- a/spec/lib/gitlab/utils/deep_size_spec.rb
+++ b/spec/lib/gitlab/utils/deep_size_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Utils::DeepSize do
let(:data) do
@@ -58,10 +58,4 @@ RSpec.describe Gitlab::Utils::DeepSize do
it { is_expected.not_to be_valid }
end
end
-
- describe '.human_default_max_size' do
- it 'returns 1 MB' do
- expect(described_class.human_default_max_size).to eq('1 MB')
- end
- end
end
diff --git a/spec/lib/gitlab/utils/delegator_override_spec.rb b/spec/lib/gitlab/utils/delegator_override_spec.rb
index af4c7fa5d8e..2dafa75e344 100644
--- a/spec/lib/gitlab/utils/delegator_override_spec.rb
+++ b/spec/lib/gitlab/utils/delegator_override_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Gitlab::Utils::DelegatorOverride do
before do
stub_env('STATIC_VERIFICATION', 'true')
+ described_class.validators.clear
end
describe '.delegator_target' do
diff --git a/spec/lib/gitlab/utils/execution_tracker_spec.rb b/spec/lib/gitlab/utils/execution_tracker_spec.rb
new file mode 100644
index 00000000000..6c42863658c
--- /dev/null
+++ b/spec/lib/gitlab/utils/execution_tracker_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Utils::ExecutionTracker do
+ subject(:tracker) { described_class.new }
+
+ describe '#over_limit?' do
+ it 'is true when max runtime is exceeded' do
+ monotonic_time_before = 1 # this will be the start time
+ monotonic_time_after = described_class::MAX_RUNTIME.to_i + 1 # this will be returned when over_limit? is called
+
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
+
+ tracker
+
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when max runtime is not exceeded' do
+ expect(tracker).not_to be_over_limit
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils/json_size_estimator_spec.rb b/spec/lib/gitlab/utils/json_size_estimator_spec.rb
index 5fd66caa5e9..ba49cc3a847 100644
--- a/spec/lib/gitlab/utils/json_size_estimator_spec.rb
+++ b/spec/lib/gitlab/utils/json_size_estimator_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Utils::JsonSizeEstimator do
RSpec::Matchers.define :match_json_bytesize_of do |expected|
diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb
index acc5bd47c8c..0a7d1160bbc 100644
--- a/spec/lib/gitlab/utils/markdown_spec.rb
+++ b/spec/lib/gitlab/utils/markdown_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Utils::Markdown do
let(:klass) do
diff --git a/spec/lib/gitlab/utils/merge_hash_spec.rb b/spec/lib/gitlab/utils/merge_hash_spec.rb
index 11daa05c9ee..4eec6e83be2 100644
--- a/spec/lib/gitlab/utils/merge_hash_spec.rb
+++ b/spec/lib/gitlab/utils/merge_hash_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Utils::MergeHash do
describe '.crush' do
it 'can flatten a hash to each element' do
diff --git a/spec/lib/gitlab/utils/nokogiri_spec.rb b/spec/lib/gitlab/utils/nokogiri_spec.rb
index 90f137f53c8..7b4c63f9168 100644
--- a/spec/lib/gitlab/utils/nokogiri_spec.rb
+++ b/spec/lib/gitlab/utils/nokogiri_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
RSpec.describe Gitlab::Utils::Nokogiri do
describe '#css_to_xpath' do
diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb
index 3ab592dfc62..1fc10bc3aa8 100644
--- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb
+++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'html/pipeline'
+require 'addressable'
RSpec.describe Gitlab::Utils::SanitizeNodeLink do
let(:klass) do
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index ad1a65ffae8..61323f0646b 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -174,7 +174,7 @@ RSpec.describe Gitlab::Utils do
{
'TEST' => 'test',
'project_with_underscores' => 'project-with-underscores',
- 'namespace/project' => 'namespace-project',
+ 'namespace/project' => 'namespace-project',
'a' * 70 => 'a' * 63,
'test_trailing_' => 'test-trailing'
}.each do |original, expected|
diff --git a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
index 7d96adf95e8..8d4629bf48b 100644
--- a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
+++ b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
@@ -150,6 +150,29 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do
}
)
end
+
+ context 'when the FF ci_variables_refactoring_to_variable is disabled' do
+ let(:entry_without_ff) { described_class.new(config, with_image_ports: true) }
+
+ before do
+ stub_feature_flags(ci_variables_refactoring_to_variable: false)
+ entry_without_ff.compose!
+ end
+
+ it 'returns correct value' do
+ expect(entry_without_ff.value)
+ .to eq(
+ tag_list: ['webide'],
+ job_variables: [{ key: 'KEY', value: 'value', public: true }],
+ options: {
+ image: { name: "image:1.0" },
+ services: [{ name: "mysql" }],
+ before_script: %w[ls pwd],
+ script: ['sleep 100']
+ }
+ )
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/word_diff/chunk_collection_spec.rb b/spec/lib/gitlab/word_diff/chunk_collection_spec.rb
index 73e9ff3974a..f76c4213c19 100644
--- a/spec/lib/gitlab/word_diff/chunk_collection_spec.rb
+++ b/spec/lib/gitlab/word_diff/chunk_collection_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::ChunkCollection do
subject(:collection) { described_class.new }
diff --git a/spec/lib/gitlab/word_diff/line_processor_spec.rb b/spec/lib/gitlab/word_diff/line_processor_spec.rb
index f448f5b5eb6..7246ed772f8 100644
--- a/spec/lib/gitlab/word_diff/line_processor_spec.rb
+++ b/spec/lib/gitlab/word_diff/line_processor_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::LineProcessor do
subject(:line_processor) { described_class.new(line) }
diff --git a/spec/lib/gitlab/word_diff/parser_spec.rb b/spec/lib/gitlab/word_diff/parser_spec.rb
index e793e44fd45..18109a8160b 100644
--- a/spec/lib/gitlab/word_diff/parser_spec.rb
+++ b/spec/lib/gitlab/word_diff/parser_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::Parser do
subject(:parser) { described_class.new }
@@ -42,18 +42,18 @@ RSpec.describe Gitlab::WordDiff::Parser do
{ index: 1, old_pos: 2, new_pos: 2, text: 'Unchanged line', type: nil, marker_ranges: [] },
{ index: 2, old_pos: 3, new_pos: 3, text: '', type: nil, marker_ranges: [] },
{ index: 3, old_pos: 4, new_pos: 4, text: 'Old changeNew addition unchanged content', type: nil,
- marker_ranges: [
- Gitlab::MarkerRange.new(0, 9, mode: :deletion),
- Gitlab::MarkerRange.new(10, 21, mode: :addition)
- ] },
+ marker_ranges: [
+ Gitlab::MarkerRange.new(0, 9, mode: :deletion),
+ Gitlab::MarkerRange.new(10, 21, mode: :addition)
+ ] },
{ index: 4, old_pos: 50, new_pos: 50, text: '@@ -50,14 +50,13 @@', type: 'match', marker_ranges: [] },
{ index: 5, old_pos: 50, new_pos: 50, text: 'First change same same same_removed_added_end of the line', type: nil,
- marker_ranges: [
- Gitlab::MarkerRange.new(0, 11, mode: :addition),
- Gitlab::MarkerRange.new(28, 35, mode: :deletion),
- Gitlab::MarkerRange.new(36, 41, mode: :addition)
- ] },
+ marker_ranges: [
+ Gitlab::MarkerRange.new(0, 11, mode: :addition),
+ Gitlab::MarkerRange.new(28, 35, mode: :deletion),
+ Gitlab::MarkerRange.new(36, 41, mode: :addition)
+ ] },
{ index: 6, old_pos: 51, new_pos: 51, text: '', type: nil, marker_ranges: [] }
]
diff --git a/spec/lib/gitlab/word_diff/positions_counter_spec.rb b/spec/lib/gitlab/word_diff/positions_counter_spec.rb
index e2c246f6801..32ce7c50591 100644
--- a/spec/lib/gitlab/word_diff/positions_counter_spec.rb
+++ b/spec/lib/gitlab/word_diff/positions_counter_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::PositionsCounter do
subject(:counter) { described_class.new }
diff --git a/spec/lib/gitlab/word_diff/segments/chunk_spec.rb b/spec/lib/gitlab/word_diff/segments/chunk_spec.rb
index 797cc42a03c..75c0e5b4a77 100644
--- a/spec/lib/gitlab/word_diff/segments/chunk_spec.rb
+++ b/spec/lib/gitlab/word_diff/segments/chunk_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::Segments::Chunk do
subject(:chunk) { described_class.new(line) }
diff --git a/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb b/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb
index 5250e6d73c2..a65f55c716f 100644
--- a/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb
+++ b/spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::Segments::DiffHunk do
subject(:diff_hunk) { described_class.new(line) }
diff --git a/spec/lib/gitlab/word_diff/segments/newline_spec.rb b/spec/lib/gitlab/word_diff/segments/newline_spec.rb
index ed5054844f1..4c0cf0c5ee4 100644
--- a/spec/lib/gitlab/word_diff/segments/newline_spec.rb
+++ b/spec/lib/gitlab/word_diff/segments/newline_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::WordDiff::Segments::Newline do
subject(:newline) { described_class.new }
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 703a4b5399e..5c9a3cc0a24 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -365,7 +365,7 @@ RSpec.describe Gitlab::Workhorse do
it 'set and notify' do
expect(Gitlab::Redis::SharedState).to receive(:with).and_call_original
expect_any_instance_of(::Redis).to receive(:publish)
- .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value")
+ .with(described_class::NOTIFICATION_PREFIX + 'test-key', "test-value")
subject
end
diff --git a/spec/lib/gitlab_edition_spec.rb b/spec/lib/gitlab_edition_spec.rb
index 6fc4312252d..46be1471896 100644
--- a/spec/lib/gitlab_edition_spec.rb
+++ b/spec/lib/gitlab_edition_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
RSpec.describe GitlabEdition do
def remove_instance_variable(ivar)
@@ -27,7 +28,57 @@ RSpec.describe GitlabEdition do
end
end
- describe 'extensions' do
+ describe '.path_glob' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:root) { described_class.root.to_s }
+
+ subject { described_class.path_glob(path) }
+
+ before do
+ allow(described_class).to receive(:jh?).and_return(jh)
+ allow(described_class).to receive(:ee?).and_return(ee)
+ end
+
+ where(:ee, :jh, :path, :expected) do
+ false | false | nil | ''
+ true | false | nil | '{,ee/}'
+ true | true | nil | '{,ee/,jh/}'
+ false | true | nil | '{,ee/,jh/}'
+ false | false | 'app/models' | 'app/models'
+ true | false | 'app/models' | '{,ee/}app/models'
+ true | true | 'app/models' | '{,ee/,jh/}app/models'
+ false | true | 'app/models' | '{,ee/,jh/}app/models'
+ end
+
+ with_them do
+ it { is_expected.to eq("#{root}/#{expected}") }
+ end
+ end
+
+ describe '.extension_path_prefixes' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.extension_path_prefixes }
+
+ before do
+ allow(described_class).to receive(:jh?).and_return(jh)
+ allow(described_class).to receive(:ee?).and_return(ee)
+ end
+
+ where(:ee, :jh, :expected) do
+ false | false | ''
+ true | false | '{,ee/}'
+ true | true | '{,ee/,jh/}'
+ false | true | '{,ee/,jh/}'
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+ end
+ end
+
+ describe '.extensions' do
context 'when .jh? is true' do
before do
allow(described_class).to receive(:jh?).and_return(true)
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index aeca7b09a88..0f117f495d1 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -306,7 +306,7 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
.with({ 'role': 'roles/storage.admin', 'members': ["serviceAccount:#{mock_email}"] })
expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
- .with({ 'role': 'roles/cloudsql.admin', 'members': ["serviceAccount:#{mock_email}"] })
+ .with({ 'role': 'roles/cloudsql.client', 'members': ["serviceAccount:#{mock_email}"] })
expect(Google::Apis::CloudresourcemanagerV1::Binding).to receive(:new)
.with({ 'role': 'roles/browser', 'members': ["serviceAccount:#{mock_email}"] })
diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb
index 59add4e8347..5f405e71d79 100644
--- a/spec/lib/marginalia_spec.rb
+++ b/spec/lib/marginalia_spec.rb
@@ -45,8 +45,8 @@ RSpec.describe 'Marginalia spec' do
let(:component_map) do
{
- "application" => "test",
- "endpoint_id" => "MarginaliaTestController#first_user",
+ "application" => "test",
+ "endpoint_id" => "MarginaliaTestController#first_user",
"correlation_id" => correlation_id,
"db_config_name" => "main"
}
@@ -62,8 +62,8 @@ RSpec.describe 'Marginalia spec' do
let(:recorded) { ActiveRecord::QueryRecorder.new { make_request(correlation_id, :first_ci_pipeline) } }
let(:component_map) do
{
- "application" => "test",
- "endpoint_id" => "MarginaliaTestController#first_ci_pipeline",
+ "application" => "test",
+ "endpoint_id" => "MarginaliaTestController#first_ci_pipeline",
"correlation_id" => correlation_id,
"db_config_name" => 'ci'
}
@@ -104,10 +104,10 @@ RSpec.describe 'Marginalia spec' do
let(:component_map) do
{
- "application" => "sidekiq",
- "endpoint_id" => "MarginaliaTestJob",
+ "application" => "sidekiq",
+ "endpoint_id" => "MarginaliaTestJob",
"correlation_id" => sidekiq_job['correlation_id'],
- "jid" => sidekiq_job['jid'],
+ "jid" => sidekiq_job['jid'],
"db_config_name" => "main"
}
end
@@ -129,9 +129,9 @@ RSpec.describe 'Marginalia spec' do
let(:component_map) do
{
- "application" => "sidekiq",
- "endpoint_id" => "ActionMailer::MailDeliveryJob",
- "jid" => delivery_job.job_id,
+ "application" => "sidekiq",
+ "endpoint_id" => "ActionMailer::MailDeliveryJob",
+ "jid" => delivery_job.job_id,
"db_config_name" => "main"
}
end
diff --git a/spec/lib/microsoft_teams/activity_spec.rb b/spec/lib/microsoft_teams/activity_spec.rb
index d1eac7204a6..08f71985a2a 100644
--- a/spec/lib/microsoft_teams/activity_spec.rb
+++ b/spec/lib/microsoft_teams/activity_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe MicrosoftTeams::Activity do
subject { described_class.new(title: 'title', subtitle: 'subtitle', text: 'text', image: 'image') }
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 18a58522d12..1629aec89f5 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -342,84 +342,68 @@ RSpec.describe ObjectStorage::DirectUpload do
context 'when length is unknown' do
let(:has_length) { false }
- context 'when s3_omit_multipart_urls feature flag is enabled' do
- let(:consolidated_settings) { true }
-
- it 'omits multipart URLs' do
- expect(subject).not_to have_key(:MultipartUpload)
- end
-
- it_behaves_like 'a valid upload'
- end
-
- context 'when s3_omit_multipart_urls feature flag is disabled' do
+ it_behaves_like 'a valid S3 upload with multipart data' do
before do
- stub_feature_flags(s3_omit_multipart_urls: false)
+ stub_object_storage_multipart_init(storage_url, "myUpload")
end
- it_behaves_like 'a valid S3 upload with multipart data' do
- before do
- stub_object_storage_multipart_init(storage_url, "myUpload")
- end
-
- context 'when maximum upload size is 0' do
- let(:maximum_size) { 0 }
+ context 'when maximum upload size is 0' do
+ let(:maximum_size) { 0 }
- it 'returns maximum number of parts' do
- expect(subject[:MultipartUpload][:PartURLs].length).to eq(100)
- end
+ it 'returns maximum number of parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(100)
+ end
- it 'part size is minimum, 5MB' do
- expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
- end
+ it 'part size is minimum, 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
end
+ end
- context 'when maximum upload size is < 5 MB' do
- let(:maximum_size) { 1024 }
+ context 'when maximum upload size is < 5 MB' do
+ let(:maximum_size) { 1024 }
- it 'returns only 1 part' do
- expect(subject[:MultipartUpload][:PartURLs].length).to eq(1)
- end
+ it 'returns only 1 part' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(1)
+ end
- it 'part size is minimum, 5MB' do
- expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
- end
+ it 'part size is minimum, 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
end
+ end
- context 'when maximum upload size is 10MB' do
- let(:maximum_size) { 10.megabyte }
+ context 'when maximum upload size is 10MB' do
+ let(:maximum_size) { 10.megabyte }
- it 'returns only 2 parts' do
- expect(subject[:MultipartUpload][:PartURLs].length).to eq(2)
- end
+ it 'returns only 2 parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(2)
+ end
- it 'part size is minimum, 5MB' do
- expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
- end
+ it 'part size is minimum, 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
end
+ end
- context 'when maximum upload size is 12MB' do
- let(:maximum_size) { 12.megabyte }
+ context 'when maximum upload size is 12MB' do
+ let(:maximum_size) { 12.megabyte }
- it 'returns only 3 parts' do
- expect(subject[:MultipartUpload][:PartURLs].length).to eq(3)
- end
+ it 'returns only 3 parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(3)
+ end
- it 'part size is rounded-up to 5MB' do
- expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
- end
+ it 'part size is rounded-up to 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
end
+ end
- context 'when maximum upload size is 49GB' do
- let(:maximum_size) { 49.gigabyte }
+ context 'when maximum upload size is 49GB' do
+ let(:maximum_size) { 49.gigabyte }
- it 'returns maximum, 100 parts' do
- expect(subject[:MultipartUpload][:PartURLs].length).to eq(100)
- end
+ it 'returns maximum, 100 parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(100)
+ end
- it 'part size is rounded-up to 5MB' do
- expect(subject[:MultipartUpload][:PartSize]).to eq(505.megabyte)
- end
+ it 'part size is rounded-up to 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(505.megabyte)
end
end
end
diff --git a/spec/lib/omni_auth/strategies/bitbucket_spec.rb b/spec/lib/omni_auth/strategies/bitbucket_spec.rb
new file mode 100644
index 00000000000..d85ce71d60a
--- /dev/null
+++ b/spec/lib/omni_auth/strategies/bitbucket_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe OmniAuth::Strategies::Bitbucket do
+ subject { described_class.new({}) }
+
+ describe '#callback_url' do
+ let(:base_url) { 'https://example.com' }
+
+ context 'when script name is not present' do
+ it 'has the correct default callback path' do
+ allow(subject).to receive(:full_host) { base_url }
+ allow(subject).to receive(:script_name).and_return('')
+ allow(subject).to receive(:query_string).and_return('')
+ expect(subject.callback_url).to eq("#{base_url}/users/auth/bitbucket/callback")
+ end
+ end
+
+ context 'when script name is present' do
+ it 'sets the callback path with script_name' do
+ allow(subject).to receive(:full_host) { base_url }
+ allow(subject).to receive(:script_name).and_return('/v1')
+ allow(subject).to receive(:query_string).and_return('')
+ expect(subject.callback_url).to eq("#{base_url}/v1/users/auth/bitbucket/callback")
+ end
+ end
+ end
+end
diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb
index a757af50dcb..5d75a6522e4 100644
--- a/spec/lib/peek/views/redis_detailed_spec.rb
+++ b/spec/lib/peek/views/redis_detailed_spec.rb
@@ -7,17 +7,19 @@ RSpec.describe Peek::Views::RedisDetailed, :request_store do
using RSpec::Parameterized::TableSyntax
- where(:cmd, :expected) do
- [:auth, 'test'] | 'auth <redacted>'
- [:set, 'key', 'value'] | 'set key <redacted>'
- [:set, 'bad'] | 'set bad'
- [:hmset, 'key1', 'value1', 'key2', 'value2'] | 'hmset key1 <redacted>'
- [:get, 'key'] | 'get key'
+ where(:commands, :expected) do
+ [[:auth, 'test']] | 'auth <redacted>'
+ [[:set, 'key', 'value']] | 'set key <redacted>'
+ [[:set, 'bad']] | 'set bad'
+ [[:hmset, 'key1', 'value1', 'key2', 'value2']] | 'hmset key1 <redacted>'
+ [[:get, 'key']] | 'get key'
+ [[:get, 'key1'], [:get, 'key2']] | 'get key1, get key2'
+ [[:set, 'key1', 'value'], [:set, 'key2', 'value']] | 'set key1 <redacted>, set key2 <redacted>'
end
with_them do
it 'scrubs Redis commands' do
- Gitlab::Instrumentation::Redis::SharedState.detail_store << { cmd: cmd, duration: 1.second }
+ Gitlab::Instrumentation::Redis::SharedState.detail_store << { commands: commands, duration: 1.second }
expect(subject.results[:details].count).to eq(1)
expect(subject.results[:details].first)
@@ -29,9 +31,9 @@ RSpec.describe Peek::Views::RedisDetailed, :request_store do
end
it 'returns aggregated results' do
- Gitlab::Instrumentation::Redis::Cache.detail_store << { cmd: [:get, 'test'], duration: 0.001 }
- Gitlab::Instrumentation::Redis::Cache.detail_store << { cmd: [:get, 'test'], duration: 1.second }
- Gitlab::Instrumentation::Redis::SharedState.detail_store << { cmd: [:get, 'test'], duration: 1.second }
+ Gitlab::Instrumentation::Redis::Cache.detail_store << { commands: [[:get, 'test']], duration: 0.001 }
+ Gitlab::Instrumentation::Redis::Cache.detail_store << { commands: [[:get, 'test']], duration: 1.second }
+ Gitlab::Instrumentation::Redis::SharedState.detail_store << { commands: [[:get, 'test']], duration: 1.second }
expect(subject.results[:calls]).to eq(3)
expect(subject.results[:duration]).to eq('2001.00ms')
diff --git a/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb b/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb
index bdf9673a53f..f93066e82be 100644
--- a/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb
+++ b/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tmpdir'
RSpec.describe Prometheus::CleanupMultiprocDirService do
describe '#execute' do
diff --git a/spec/lib/security/ci_configuration/sast_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
index 611a886d252..381ea60e7f5 100644
--- a/spec/lib/security/ci_configuration/sast_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
@@ -33,12 +33,12 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
params.merge( { analyzers:
[
{
- name: "bandit",
- enabled: false
+ name: "bandit",
+ enabled: false
},
{
- name: "brakeman",
- enabled: true,
+ name: "brakeman",
+ enabled: true,
variables: [
{ field: "SAST_BRAKEMAN_LEVEL",
default_value: "1",
@@ -46,8 +46,8 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
]
},
{
- name: "flawfinder",
- enabled: true,
+ name: "flawfinder",
+ enabled: true,
variables: [
{ field: "SAST_FLAWFINDER_LEVEL",
default_value: "1",
@@ -62,12 +62,12 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
params.merge( { analyzers:
[
{
- name: "flawfinder",
- enabled: true
+ name: "flawfinder",
+ enabled: true
},
{
- name: "brakeman",
- enabled: true
+ name: "brakeman",
+ enabled: true
}
] }
)
@@ -219,49 +219,49 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
def existing_gitlab_ci_and_template_array_without_sast
{ "stages" => %w(test security),
- "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
- "include" => [{ "template" => "existing.yml" }] }
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "existing.yml" }] }
end
def existing_gitlab_ci_and_single_template_with_sast_and_default_stage
{ "stages" => %w(test),
- "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
- "include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
+ "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
+ "include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
end
def existing_gitlab_ci_and_single_template_without_sast
{ "stages" => %w(test security),
- "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
- "include" => { "template" => "existing.yml" } }
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => { "template" => "existing.yml" } }
end
def existing_gitlab_ci_with_no_variables
{ "stages" => %w(test security),
- "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
- "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci_with_no_sast_section
{ "stages" => %w(test security),
- "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci_with_no_sast_variables
{ "stages" => %w(test security),
- "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "stage" => "security" },
- "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci
{ "stages" => %w(test security),
- "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
- "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
- "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
end
diff --git a/spec/lib/security/report_schema_version_matcher_spec.rb b/spec/lib/security/report_schema_version_matcher_spec.rb
index 9c40f0bc6fa..eaf49aa4744 100644
--- a/spec/lib/security/report_schema_version_matcher_spec.rb
+++ b/spec/lib/security/report_schema_version_matcher_spec.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Security::ReportSchemaVersionMatcher do
let(:vendored_versions) { %w[14.0.0 14.0.1 14.0.2 14.1.0] }
diff --git a/spec/lib/security/weak_passwords_spec.rb b/spec/lib/security/weak_passwords_spec.rb
new file mode 100644
index 00000000000..9d12c352abf
--- /dev/null
+++ b/spec/lib/security/weak_passwords_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::WeakPasswords do
+ describe "#weak_for_user?" do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:user) do
+ build_stubbed(:user, username: "56d4ab689a_win",
+ name: "Weakést McWeaky-Pass Jr",
+ email: "predictāble.ZZZ+seventeen@examplecorp.com",
+ public_email: "fortunate@acme.com"
+ )
+ end
+
+ where(:password, :too_weak) do
+ # A random password is not too weak
+ "d2262d56" | false
+
+ # The case-insensitive weak password list
+ "password" | true
+ "pAssWord" | true
+ "princeofdarkness" | true
+
+ # Forbidden substrings
+ "A1B2gitlabC3" | true
+ "gitlab123" | true
+ "theonedevopsplatform" | true
+ "A1gitlib" | false
+
+ # Predicatable name substrings
+ "Aweakést" | true
+ "!@mCwEaKy" | true
+ "A1B2pass" | true
+ "A1B2C3jr" | false # jr is too short
+
+ # Predictable username substrings
+ "56d4ab689a" | true
+ "56d4ab689a_win" | true
+ "56d4ab68" | false # it's part of the username, but not a full part
+ "A1B2Cwin" | false # win is too short
+
+ # Predictable user.email substrings
+ "predictāble.ZZZ+seventeen@examplecorp.com" | true
+ "predictable.ZZZ+seventeen@examplecorp.com" | true
+ "predictāble.ZZZ+seventeen" | true
+ "examplecorp.com" | true
+ "!@exAmplecorp" | true
+ "predictāble123" | true
+ "seventeen" | true
+ "predictable" | false # the accent is different
+ "A1B2CZzZ" | false # ZZZ is too short
+ # Other emails are not considered
+ "fortunate@acme.com" | false
+ "A1B2acme" | false
+ "fortunate" | false
+
+ # A short password is not automatically too weak
+ # We rely on User's password length validation, not WeakPasswords.
+ "1" | false
+ "1234567" | false
+ # But a short password with forbidden words or user attributes
+ # is still weak
+ "gitlab" | true
+ "pass" | true
+ end
+
+ with_them do
+ it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) }
+ end
+
+ context 'with a user who has short email parts' do
+ before do
+ user.email = 'sid@1.io'
+ end
+
+ where(:password, :too_weak) do
+ "11111111" | true # This is on the weak password list
+ "1.ioABCD" | true # 1.io is long enough to match
+ "sid@1.io" | true # matches the email in full
+ "sid@1.ioAB" | true
+ # sid, 1, and io on their own are too short
+ "sid1ioAB" | false
+ "sidsidsi" | false
+ "ioioioio" | false
+ end
+
+ with_them do
+ it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) }
+ end
+ end
+
+ context 'with a user who is missing attributes' do
+ before do
+ user.name = nil
+ user.email = nil
+ user.username = nil
+ end
+
+ where(:password, :too_weak) do
+ "d2262d56" | false
+ "password" | true
+ "gitlab123" | true
+ end
+
+ with_them do
+ it { expect(subject.weak_for_user?(password, user)).to eq(too_weak) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/concerns/container_with_html_options_spec.rb b/spec/lib/sidebars/concerns/container_with_html_options_spec.rb
index 7f834419866..d95cdb9e0fe 100644
--- a/spec/lib/sidebars/concerns/container_with_html_options_spec.rb
+++ b/spec/lib/sidebars/concerns/container_with_html_options_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Sidebars::Concerns::ContainerWithHtmlOptions do
subject do
diff --git a/spec/lib/sidebars/concerns/link_with_html_options_spec.rb b/spec/lib/sidebars/concerns/link_with_html_options_spec.rb
index 1e890bffad1..f7e6701c37d 100644
--- a/spec/lib/sidebars/concerns/link_with_html_options_spec.rb
+++ b/spec/lib/sidebars/concerns/link_with_html_options_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Sidebars::Concerns::LinkWithHtmlOptions do
let(:options) { {} }
diff --git a/spec/lib/sidebars/groups/menus/observability_menu_spec.rb b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb
new file mode 100644
index 00000000000..3a91b1aea2f
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::ObservabilityMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:root_group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:group) { root_group }
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe '#render?' do
+ before do
+ allow(menu).to receive(:can?).and_call_original
+ end
+
+ context 'when user can :read_observability' do
+ before do
+ allow(menu).to receive(:can?).with(user, :read_observability, group).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(menu.render?).to eq true
+ end
+ end
+
+ context 'when user cannot :read_observability' do
+ before do
+ allow(menu).to receive(:can?).with(user, :read_observability, group).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(menu.render?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
index d3cb18222b5..c5666724acf 100644
--- a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
end
end
+ let_it_be(:harbor_integration) { create(:harbor_integration, group: group, project: nil) }
+
let(:user) { owner }
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
let(:menu) { described_class.new(context) }
diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
index 252da8ea699..4e3c639672b 100644
--- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu do
it_behaves_like 'access rights checks'
end
- describe 'Packages & Registries' do
+ describe 'Packages and registries' do
let(:item_id) { :packages_and_registries }
before do
diff --git a/spec/lib/sidebars/menu_item_spec.rb b/spec/lib/sidebars/menu_item_spec.rb
index 3adde64f550..15804f51934 100644
--- a/spec/lib/sidebars/menu_item_spec.rb
+++ b/spec/lib/sidebars/menu_item_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Sidebars::MenuItem do
let(:title) { 'foo' }
diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
index 56eb082e101..90ff04a2064 100644
--- a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project, reload: true) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
@@ -37,6 +37,40 @@ RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
specify { is_expected.to be_nil }
end
+
+ describe 'when the feature is disabled' do
+ before do
+ project.update_attribute("#{item_id}_access_level", 'disabled')
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ describe 'when split_operations_visibility_permissions FF is disabled' do
+ before do
+ stub_feature_flags(split_operations_visibility_permissions: false)
+ end
+
+ it { is_expected.not_to be_nil }
+
+ context 'and the feature is disabled' do
+ before do
+ project.update_attribute("#{item_id}_access_level", 'disabled')
+ end
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context 'and operations is disabled' do
+ before do
+ project.update_attribute(:operations_access_level, 'disabled')
+ end
+
+ it do
+ is_expected.to be_nil if [:environments, :feature_flags].include?(item_id)
+ end
+ end
+ end
end
describe 'Feature Flags' do
diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
index 36a76e70a48..4ae29f28f3a 100644
--- a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
@@ -68,13 +68,11 @@ RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do
end
describe '#pill_count' do
- before do
- expect_next_instance_of(LearnGitlab::Onboarding) do |onboarding|
- expect(onboarding).to receive(:completed_percentage).and_return(20)
+ it 'returns pill count' do
+ expect_next_instance_of(Onboarding::Completion) do |onboarding|
+ expect(onboarding).to receive(:percentage).and_return(20)
end
- end
- it 'returns pill count' do
expect(subject.pill_count).to eq '20%'
end
end
diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
index ba5137e2b92..bd0904b9db2 100644
--- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
@@ -12,11 +12,28 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do
subject { described_class.new(context) }
describe '#render?' do
- context 'when operations feature is disabled' do
- it 'returns false' do
- project.project_feature.update!(operations_access_level: Featurable::DISABLED)
+ using RSpec::Parameterized::TableSyntax
+ let(:enabled) { Featurable::PRIVATE }
+ let(:disabled) { Featurable::DISABLED }
+
+ where(:flag_enabled, :operations_access_level, :monitor_level, :render) do
+ true | ref(:disabled) | ref(:enabled) | true
+ true | ref(:disabled) | ref(:disabled) | false
+ true | ref(:enabled) | ref(:enabled) | true
+ true | ref(:enabled) | ref(:disabled) | false
+ false | ref(:disabled) | ref(:enabled) | false
+ false | ref(:disabled) | ref(:disabled) | false
+ false | ref(:enabled) | ref(:enabled) | true
+ false | ref(:enabled) | ref(:disabled) | true
+ end
+
+ with_them do
+ it 'renders when expected to' do
+ stub_feature_flags(split_operations_visibility_permissions: flag_enabled)
+ project.project_feature.update!(operations_access_level: operations_access_level)
+ project.project_feature.update!(monitor_access_level: monitor_level)
- expect(subject.render?).to be false
+ expect(subject.render?).to be render
end
end
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index 9b78fc807bf..6491ef823e9 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
let_it_be(:project) { create(:project) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
+
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
@@ -65,7 +67,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
describe 'Packages Registry' do
let(:item_id) { :packages_registry }
- context 'when user can read packages' do
+ shared_examples 'when user can read packages' do
context 'when config package setting is disabled' do
it 'the menu item is not added to list of menu items' do
stub_config(packages: { enabled: false })
@@ -83,13 +85,25 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
end
end
- context 'when user cannot read packages' do
+ shared_examples 'when user cannot read packages' do
let(:user) { nil }
it 'the menu item is not added to list of menu items' do
is_expected.to be_nil
end
end
+
+ it_behaves_like 'when user can read packages'
+ it_behaves_like 'when user cannot read packages'
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(read_package_policy_rule: false)
+ end
+
+ it_behaves_like 'when user can read packages'
+ it_behaves_like 'when user cannot read packages'
+ end
end
describe 'Container Registry' do
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index f41f7a01d88..0733e0c6521 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -133,7 +133,13 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
end
end
- describe 'Packages & Registries' do
+ describe 'Merge requests' do
+ let(:item_id) { :merge_requests }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'Packages and registries' do
let(:item_id) { :packages_and_registries }
let(:packages_enabled) { false }
diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb
index 59b2fc519ae..241c3b33777 100644
--- a/spec/lib/system_check/base_check_spec.rb
+++ b/spec/lib/system_check/base_check_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe SystemCheck::BaseCheck do
context 'helpers on instance level' do
diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb
index 3a2eb105964..1ac989cc46b 100644
--- a/spec/mailers/emails/pipelines_spec.rb
+++ b/spec/mailers/emails/pipelines_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Emails::Pipelines do
let!(:merge_request) do
create(:merge_request, source_branch: 'master', target_branch: 'feature',
- source_project: project, target_project: project)
+ source_project: project, target_project: project)
end
it 'has correct information that there is no merge request link' do
@@ -56,7 +56,7 @@ RSpec.describe Emails::Pipelines do
context 'when branch pipeline is set to a merge request as a head pipeline' do
let(:pipeline) do
create(:ci_pipeline, project: project, ref: ref, sha: sha,
- merge_requests_as_head_pipeline: [merge_request])
+ merge_requests_as_head_pipeline: [merge_request])
end
let(:merge_request) do
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index 28011456a66..1523d9b986b 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Emails::ServiceDesk do
shared_examples 'read template from repository' do |template_key|
let(:template_content) { 'custom text' }
- let(:issue) { create(:issue, project: project)}
+ let(:issue) { create(:issue, project: project) }
before do
issue.issue_email_participants.create!(email: email)
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 8beb54bca4d..1f53c472c5c 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -167,6 +167,17 @@ RSpec.describe Notify do
is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
end
end
+
+ context 'when sent with a non default locale' do
+ let(:email_obj) { create(:email, :confirmed, user_id: recipient.id, email: '123@abc') }
+ let(:recipient) { create(:user, preferred_language: :zh_CN) }
+
+ it 'is translated into zh_CN' do
+ recipient.notification_email = email_obj.email
+ recipient.save!
+ is_expected.to have_body_text '指派人从 <strong>Previous Assignee</strong> 更改为 <strong>John Doe</strong>'
+ end
+ end
end
describe 'that have been relabeled' do
diff --git a/spec/mailers/previews_spec.rb b/spec/mailers/previews_spec.rb
new file mode 100644
index 00000000000..14bd56e5d40
--- /dev/null
+++ b/spec/mailers/previews_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Mailer previews' do
+ # Setup needed for email previews
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, :import_failed, group: group, import_last_error: 'some error') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
+ let_it_be(:remote_mirror) { create(:remote_mirror, project: project) }
+ let_it_be(:member) { create(:project_member, :maintainer, project: project, created_by: user) }
+
+ Gitlab.ee do
+ let_it_be(:epic) { create(:epic, group: group) }
+ end
+
+ let(:expected_kind) { [Mail::Message, ActionMailer::MessageDelivery] }
+
+ let(:pending_failures) do
+ {
+ 'NotifyPreview#note_merge_request_email_for_diff_discussion' =>
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/372885'
+ }
+ end
+
+ subject { preview.call(email) }
+
+ where(:preview, :email) do
+ ActionMailer::Preview.all.flat_map { |preview| preview.emails.map { |email| [preview, email] } }
+ end
+
+ with_them do
+ it do
+ issue_link = pending_failures["#{preview.name}##{email}"]
+ pending "See #{issue_link}" if issue_link
+
+ is_expected.to be_kind_of(Mail::Message).or(be_kind_of(ActionMailer::MessageDelivery))
+ end
+ end
+end
diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb
index 8b1bc33d8be..5edd9c2d023 100644
--- a/spec/mailers/repository_check_mailer_spec.rb
+++ b/spec/mailers/repository_check_mailer_spec.rb
@@ -14,6 +14,15 @@ RSpec.describe RepositoryCheckMailer do
expect(mail).to deliver_to admins.map(&:email)
end
+ it 'email with I18n.default_locale' do
+ admins = [create(:admin, preferred_language: :zh_CN), create(:admin, preferred_language: :zh_CN)]
+
+ mail = described_class.notify(3)
+
+ expect(mail).to deliver_to admins.map(&:email)
+ expect(mail).to have_subject 'GitLab Admin | 3 projects failed their last repository check'
+ end
+
it 'omits blocked admins' do
blocked = create(:admin, :blocked)
admins = create_list(:admin, 3)
diff --git a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
index 6df8e1b2ebf..ae510826fe1 100644
--- a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
+++ b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe CreateBaseWorkItemTypes, :migration do
let(:base_types) do
{
- issue: 0,
- incident: 1,
- test_case: 2,
+ issue: 0,
+ incident: 1,
+ test_case: 2,
requirement: 3
}
end
diff --git a/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb b/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
index f734456b0b6..c88f94c6426 100644
--- a/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
+++ b/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe RemoveDuplicateProjectAuthorizations, :migration do
project_authorizations.create! project_id: project_1.id, user_id: user_1.id, access_level: Gitlab::Access::REPORTER
end
- it { expect { subject }.to change { ProjectAuthorization.count}.from(3).to(1) }
+ it { expect { subject }.to change { ProjectAuthorization.count }.from(3).to(1) }
it 'retains the highest access level' do
subject
diff --git a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
index 1957a973ee1..552602983d9 100644
--- a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
+++ b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe UpsertBaseWorkItemTypes, :migration do
let(:base_types) do
{
- issue: 0,
- incident: 1,
- test_case: 2,
+ issue: 0,
+ incident: 1,
+ test_case: 2,
requirement: 3
}
end
diff --git a/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb b/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
index c90eabbe4eb..69ee10eb0d1 100644
--- a/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
+++ b/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe UpdateReportTypeForExistingApprovalProjectRules, :migration do
end
context 'with the rule name set to another value (e.g., Test Rule)' do
- let(:rule_name) { 'Test Rule'}
+ let(:rule_name) { 'Test Rule' }
it 'does not update report_type' do
expect { migrate! }.not_to change { approval_project_rule.reload.report_type }
diff --git a/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb b/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
index 6a82ed016af..ef6dd94d9e3 100644
--- a/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
+++ b/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
@@ -33,11 +33,11 @@ RSpec.describe MigrateRemainingU2fRegistrations, :migration do
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5), { key_handle: SecureRandom.random_bytes(255) })
public_key ||= Base64.strict_encode64(device.origin_public_key_raw)
u2f_registrations.create!({ id: id,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: public_key,
- counter: 5,
- name: name,
- user_id: user.id })
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: public_key,
+ counter: 5,
+ name: name,
+ user_id: user.id })
end
end
diff --git a/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb b/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb
index b80e4703f07..34a6e2fdd12 100644
--- a/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb
+++ b/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb
@@ -10,11 +10,11 @@ RSpec.describe AddTaskToWorkItemTypes, :migration do
let(:base_types) do
{
- issue: 0,
- incident: 1,
- test_case: 2,
+ issue: 0,
+ incident: 1,
+ test_case: 2,
requirement: 3,
- task: 4
+ task: 4
}
end
diff --git a/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb b/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb
new file mode 100644
index 00000000000..44e80980b27
--- /dev/null
+++ b/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleRemoveSelfManagedWikiNotes do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :notes,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ }
+ end
+ end
+
+ context 'with com? or staging?' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ allow(::Gitlab).to receive(:staging?).and_return(false)
+ end
+
+ it 'does not schedule new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb b/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb
new file mode 100644
index 00000000000..5921dd64c0e
--- /dev/null
+++ b/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixIncorrectJobArtifactsExpireAt, migration: :gitlab_ci do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ it 'does not schedule background jobs when Gitlab.com is true' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+ end
+ end
+
+ it 'schedules background job on non Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(false)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_job_artifacts,
+ column_name: :id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb b/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
new file mode 100644
index 00000000000..fdd97f2d008
--- /dev/null
+++ b/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenceForRecentPublicProjects, schema: 20220801155858 do
+ context 'when on gitlab.com' do
+ let(:background_migration) { described_class::MIGRATION }
+ let(:migration) { described_class.new }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ migration.up
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of projects' do
+ expect(background_migration).to(
+ have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migration.down
+
+ expect(described_class::MIGRATION).not_to have_scheduled_batched_migration
+ end
+ end
+ end
+
+ context 'when on self-managed instances' do
+ let(:migration) { described_class.new }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ describe '#up' do
+ it 'does not schedule background job' do
+ expect(migration).not_to receive(:queue_batched_background_migration)
+
+ migration.up
+ end
+ end
+
+ describe '#down' do
+ it 'does not delete background job' do
+ expect(migration).not_to receive(:delete_batched_background_migration)
+
+ migration.down
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220809002011_schedule_destroy_invalid_group_members_spec.rb b/spec/migrations/20220809002011_schedule_destroy_invalid_group_members_spec.rb
new file mode 100644
index 00000000000..31dd4344d9f
--- /dev/null
+++ b/spec/migrations/20220809002011_schedule_destroy_invalid_group_members_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleDestroyInvalidGroupMembers, :migration do
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of members' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :members,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb b/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb
new file mode 100644
index 00000000000..5a5e2362a53
--- /dev/null
+++ b/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe UpdateStartDateForIterationsCadences, :freeze_time do
+ let(:migration) { described_class.new }
+ let(:namespaces) { table(:namespaces) }
+ let(:sprints) { table(:sprints) }
+ let(:iterations_cadences) { table(:iterations_cadences) }
+
+ let!(:group1) { namespaces.create!(name: 'abc', path: 'abc') }
+ let!(:group2) { namespaces.create!(name: 'def', path: 'def') }
+
+ let(:first_upcoming_start_date) { Date.current + 2.weeks }
+ let(:original_cadence_start_date) { Date.current - 1.week }
+
+ # rubocop: disable Layout/LineLength
+ let!(:auto_cadence1) { iterations_cadences.create!(start_date: original_cadence_start_date, group_id: group1.id, title: "ic") }
+ let!(:auto_cadence2) { iterations_cadences.create!(start_date: original_cadence_start_date, group_id: group1.id, title: "ic") }
+ let!(:auto_cadence3) { iterations_cadences.create!(start_date: nil, group_id: group2.id, title: "ic") }
+ let!(:manual_cadence1) { iterations_cadences.create!(start_date: Date.current, group_id: group1.id, automatic: false, title: "ic") }
+ let!(:manual_cadence2) { iterations_cadences.create!(start_date: Date.current, group_id: group2.id, automatic: false, title: "ic") }
+ # rubocop: enable Layout/LineLength
+
+ def cadence_params(cadence)
+ { iterations_cadence_id: cadence.id, group_id: cadence.group_id }
+ end
+
+ before do
+ # Past iteratioin
+ sprints.create!(id: 1, iid: 1, **cadence_params(auto_cadence1),
+ start_date: Date.current - 1.week, due_date: Date.current - 1.day)
+ # Current iteraition
+ sprints.create!(id: 3, iid: 5, **cadence_params(auto_cadence1),
+ start_date: Date.current, due_date: Date.current + 1.week)
+ # First upcoming iteration
+ sprints.create!(id: 4, iid: 8, **cadence_params(auto_cadence1),
+ start_date: first_upcoming_start_date, due_date: first_upcoming_start_date + 1.week)
+ # Second upcoming iteration
+ sprints.create!(id: 5, iid: 9, **cadence_params(auto_cadence1),
+ start_date: first_upcoming_start_date + 2.weeks, due_date: first_upcoming_start_date + 3.weeks)
+
+ sprints.create!(id: 6, iid: 1, **cadence_params(manual_cadence2),
+ start_date: Date.current, due_date: Date.current + 1.week)
+ sprints.create!(id: 7, iid: 5, **cadence_params(manual_cadence2),
+ start_date: Date.current + 2.weeks, due_date: Date.current + 3.weeks)
+ end
+
+ describe '#up' do
+ it "updates the start date of an automatic cadence to the start date of its first upcoming sprint record." do
+ expect { migration.up }
+ .to change { auto_cadence1.reload.start_date }.to(first_upcoming_start_date)
+ .and not_change { auto_cadence2.reload.start_date } # the cadence doesn't have any upcoming iteration.
+ .and not_change { auto_cadence3.reload.start_date } # the cadence is empty; it has no iterations.
+ .and not_change { manual_cadence1.reload.start_date } # manual cadence don't need to be touched.
+ .and not_change { manual_cadence2.reload.start_date } # manual cadence don't need to be touched.
+ end
+ end
+
+ describe '#down' do
+ it "updates the start date of an automatic cadence to the start date of its earliest sprint record." do
+ migration.up
+
+ expect { migration.down }
+ .to change { auto_cadence1.reload.start_date }.to(original_cadence_start_date)
+ .and not_change { auto_cadence2.reload.start_date } # the cadence is empty; it has no iterations.
+ .and not_change { manual_cadence1.reload.start_date } # manual cadence don't need to be touched.
+ .and not_change { manual_cadence2.reload.start_date } # manual cadence don't need to be touched.
+ end
+ end
+end
diff --git a/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb b/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
new file mode 100644
index 00000000000..c53dd9de649
--- /dev/null
+++ b/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+require_migration!
+
+RSpec.describe AddVulnerabilityAdvisoryForeignKeyToSbomVulnerableComponentVersions do
+ let(:table) { described_class::SOURCE_TABLE }
+ let(:column) { described_class::COLUMN }
+ let(:foreign_key) { -> { described_class.new.foreign_keys_for(table, column).first } }
+
+ it "creates and drops the foreign key" do
+ reversible_migration do |migration|
+ migration.before -> do
+ expect(foreign_key.call).to be(nil)
+ end
+
+ migration.after -> do
+ expect(foreign_key.call).to have_attributes(column: column.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb b/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
new file mode 100644
index 00000000000..b9cb6891681
--- /dev/null
+++ b/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+require_migration!
+
+RSpec.describe AddSbomComponentVersionForeignKeyToSbomVulnerableComponentVersions do
+ let(:table) { described_class::SOURCE_TABLE }
+ let(:column) { described_class::COLUMN }
+ let(:foreign_key) { -> { described_class.new.foreign_keys_for(table, column).first } }
+
+ it "creates and drops the foreign key" do
+ reversible_migration do |migration|
+ migration.before -> do
+ expect(foreign_key.call).to be(nil)
+ end
+
+ migration.after -> do
+ expect(foreign_key.call).to have_attributes(column: column.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220901035725_schedule_destroy_invalid_project_members_spec.rb b/spec/migrations/20220901035725_schedule_destroy_invalid_project_members_spec.rb
new file mode 100644
index 00000000000..ed9f7e3cd44
--- /dev/null
+++ b/spec/migrations/20220901035725_schedule_destroy_invalid_project_members_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleDestroyInvalidProjectMembers, :migration do
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of members' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :members,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb b/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
new file mode 100644
index 00000000000..e4ac094ab48
--- /dev/null
+++ b/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForProjectsLessThanOneMb do
+ let_it_be(:migration) { described_class.new }
+ let_it_be(:post_migration) { described_class::MIGRATION }
+
+ context 'when on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of project_settings' do
+ migration.up
+
+ expect(post_migration).to(
+ have_scheduled_batched_migration(
+ table_name: :project_settings,
+ column_name: :project_id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migration.down
+
+ expect(post_migration).not_to have_scheduled_batched_migration
+ end
+ end
+ end
+
+ context 'when on self-managed instance' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ describe '#up' do
+ it 'does not schedule background job' do
+ expect(migration).not_to receive(:queue_batched_background_migration)
+
+ migration.up
+ end
+ end
+
+ describe '#down' do
+ it 'does not delete background job' do
+ expect(migration).not_to receive(:delete_batched_background_migration)
+
+ migration.down
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb b/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb
new file mode 100644
index 00000000000..7338a6ab9ae
--- /dev/null
+++ b/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanupAttentionRequestRelatedSystemNotes, :migration do
+ let(:notes) { table(:notes) }
+ let(:system_note_metadata) { table(:system_note_metadata) }
+
+ it 'removes all notes with attention request related system_note_metadata' do
+ notes.create!(id: 1, note: 'Attention request note', noteable_type: 'MergeRequest')
+ notes.create!(id: 2, note: 'Attention request remove note', noteable_type: 'MergeRequest')
+ notes.create!(id: 3, note: 'MergeRequest note', noteable_type: 'MergeRequest')
+ notes.create!(id: 4, note: 'Commit note', noteable_type: 'Commit')
+ system_note_metadata.create!(id: 11, action: 'attention_requested', note_id: 1)
+ system_note_metadata.create!(id: 22, action: 'attention_request_removed', note_id: 2)
+ system_note_metadata.create!(id: 33, action: 'merged', note_id: 3)
+
+ expect { migrate! }.to change(notes, :count).by(-2)
+
+ expect(system_note_metadata.where(action: %w[attention_requested attention_request_removed]).size).to eq(0)
+ expect(notes.where(noteable_type: 'MergeRequest').size).to eq(1)
+ expect(notes.where(noteable_type: 'Commit').size).to eq(1)
+ expect(system_note_metadata.where(action: 'merged').size).to eq(1)
+ end
+end
diff --git a/spec/migrations/backfill_namespace_id_on_issues_spec.rb b/spec/migrations/backfill_namespace_id_on_issues_spec.rb
new file mode 100644
index 00000000000..2721d7ce8f1
--- /dev/null
+++ b/spec/migrations/backfill_namespace_id_on_issues_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillNamespaceIdOnIssues, :migration do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of issues' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :issues,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb b/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb
new file mode 100644
index 00000000000..039ee92f8bd
--- /dev/null
+++ b/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ChangeTaskSystemNoteWordingToChecklistItem, :migration do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules a batched background migration' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :system_note_metadata,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/confirm_support_bot_user_spec.rb b/spec/migrations/confirm_support_bot_user_spec.rb
index f6bcab4aa7d..c60c7fe45f7 100644
--- a/spec/migrations/confirm_support_bot_user_spec.rb
+++ b/spec/migrations/confirm_support_bot_user_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe ConfirmSupportBotUser, :migration do
end
it 'does not change the `created_at` attribute' do
- expect { migrate!}.not_to change { support_bot.reload.created_at }.from(nil)
+ expect { migrate! }.not_to change { support_bot.reload.created_at }.from(nil)
end
end
diff --git a/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb
new file mode 100644
index 00000000000..b5bb86edce2
--- /dev/null
+++ b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema do
+ let(:partitions_sql) do
+ <<~SQL
+ SELECT
+ partitions.relname AS partition_name
+ FROM pg_inherits
+ JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
+ JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid
+ WHERE
+ parent.relname = 'security_findings'
+ SQL
+ end
+
+ describe '#up' do
+ it 'changes the `security_findings` table to be partitioned' do
+ expect { migrate! }.to change { security_findings_partitioned? }.from(false).to(true)
+ .and change { execute(partitions_sql) }.from([]).to(['security_findings_1'])
+ end
+ end
+
+ describe '#down' do
+ context 'when there is a partition' do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let(:security_scans) { table(:security_scans) }
+ let(:security_findings) { table(:security_findings) }
+
+ let(:user) { users.create!(email: 'test@gitlab.com', projects_limit: 5) }
+ let(:namespace) { namespaces.create!(name: 'gtlb', path: 'gitlab', type: Namespaces::UserNamespace.sti_name) }
+ let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id, name: 'foo') }
+ let(:scanner) { scanners.create!(project_id: project.id, external_id: 'bandit', name: 'Bandit') }
+ let(:security_scan) { security_scans.create!(build_id: 1, scan_type: 1) }
+
+ let(:security_findings_count_sql) { 'SELECT COUNT(*) FROM security_findings' }
+
+ before do
+ migrate!
+
+ security_findings.create!(
+ scan_id: security_scan.id,
+ scanner_id: scanner.id,
+ uuid: SecureRandom.uuid,
+ severity: 0,
+ confidence: 0
+ )
+ end
+
+ it 'creates the original table with the data from the existing partition' do
+ expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false)
+ .and not_change { execute(security_findings_count_sql) }.from([1])
+ end
+
+ context 'when there are more than one partitions' do
+ before do
+ migrate!
+
+ execute(<<~SQL)
+ CREATE TABLE gitlab_partitions_dynamic.security_findings_11
+ PARTITION OF security_findings FOR VALUES IN (11)
+ SQL
+ end
+
+ it 'creates the original table from the latest existing partition' do
+ expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false)
+ .and change { execute(security_findings_count_sql) }.from([1]).to([0])
+ end
+ end
+ end
+
+ context 'when there is no partition' do
+ before do
+ migrate!
+
+ execute(partitions_sql).each do |partition_name|
+ execute("DROP TABLE gitlab_partitions_dynamic.#{partition_name}")
+ end
+ end
+
+ it 'creates the original table' do
+ expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false)
+ end
+ end
+ end
+
+ def security_findings_partitioned?
+ sql = <<~SQL
+ SELECT
+ COUNT(*)
+ FROM
+ pg_partitioned_table
+ INNER JOIN pg_class ON pg_class.oid = pg_partitioned_table.partrelid
+ WHERE pg_class.relname = 'security_findings'
+ SQL
+
+ execute(sql).first != 0
+ end
+
+ def execute(sql)
+ ActiveRecord::Base.connection.execute(sql).values.flatten
+ end
+end
diff --git a/spec/migrations/orphaned_invited_members_cleanup_spec.rb b/spec/migrations/orphaned_invited_members_cleanup_spec.rb
new file mode 100644
index 00000000000..4427e707f56
--- /dev/null
+++ b/spec/migrations/orphaned_invited_members_cleanup_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe OrphanedInvitedMembersCleanup, :migration do
+ describe '#up', :aggregate_failures do
+ it 'removes accepted members with no associated user' do
+ user = create_user!('testuser1')
+
+ create_member(invite_token: nil, invite_accepted_at: 1.day.ago)
+ record2 = create_member(invite_token: nil, invite_accepted_at: 1.day.ago, user_id: user.id)
+ record3 = create_member(invite_token: 'foo2', invite_accepted_at: nil)
+ record4 = create_member(invite_token: 'foo3', invite_accepted_at: 1.day.ago)
+
+ migrate!
+
+ expect(table(:members).all.pluck(:id)).to match_array([record2.id, record3.id, record4.id])
+ end
+ end
+
+ private
+
+ def create_user!(name)
+ email = "#{name}@example.com"
+
+ table(:users).create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0
+ )
+ end
+
+ def create_member(**extra_attributes)
+ defaults = {
+ access_level: 10,
+ source_id: 1,
+ source_type: "Project",
+ notification_level: 0,
+ type: 'ProjectMember'
+ }
+
+ table(:members).create!(defaults.merge(extra_attributes))
+ end
+end
diff --git a/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb b/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb
new file mode 100644
index 00000000000..126d49790a5
--- /dev/null
+++ b/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RescheduleIssueWorkItemTypeIdBackfill, :migration do
+ let_it_be(:migration) { described_class::MIGRATION }
+ let_it_be(:interval) { 2.minutes }
+ let_it_be(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
+ let_it_be(:base_work_item_type_ids) do
+ table(:work_item_types).where(namespace_id: nil).order(:base_type).each_with_object({}) do |type, hash|
+ hash[type.base_type] = type.id
+ end
+ end
+
+ describe '#up' do
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ scheduled_migrations = Gitlab::Database::BackgroundMigration::BatchedMigration.where(
+ job_class_name: migration
+ )
+ work_item_types = table(:work_item_types).where(namespace_id: nil)
+
+ expect(scheduled_migrations.count).to eq(work_item_types.count)
+
+ [:issue, :incident, :test_case, :requirement, :task].each do |issue_type|
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :issues,
+ column_name: :id,
+ job_arguments: [issue_type_enum[issue_type], base_work_item_type_ids[issue_type_enum[issue_type]]],
+ interval: interval,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ batch_class_name: described_class::BATCH_CLASS_NAME
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/reset_job_token_scope_enabled_again_spec.rb b/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
index da6817f6f21..8f9e12852e1 100644
--- a/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
+++ b/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe ResetJobTokenScopeEnabledAgain do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id)}
- let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id)}
+ let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id) }
+ let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id) }
before do
settings.create!(id: 1, project_id: project_1.id, job_token_scope_enabled: true)
diff --git a/spec/migrations/reset_job_token_scope_enabled_spec.rb b/spec/migrations/reset_job_token_scope_enabled_spec.rb
index 40dfe4de34b..fb7bd78c11f 100644
--- a/spec/migrations/reset_job_token_scope_enabled_spec.rb
+++ b/spec/migrations/reset_job_token_scope_enabled_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe ResetJobTokenScopeEnabled do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id)}
- let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id)}
+ let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id) }
+ let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id) }
before do
settings.create!(id: 1, project_id: project_1.id, job_token_scope_enabled: true)
diff --git a/spec/migrations/reset_severity_levels_to_new_default_spec.rb b/spec/migrations/reset_severity_levels_to_new_default_spec.rb
index 18dc001db16..c352f1f3cee 100644
--- a/spec/migrations/reset_severity_levels_to_new_default_spec.rb
+++ b/spec/migrations/reset_severity_levels_to_new_default_spec.rb
@@ -6,10 +6,10 @@ require_migration!
RSpec.describe ResetSeverityLevelsToNewDefault do
let(:approval_project_rules) { table(:approval_project_rules) }
- let(:projects) { table(:projects)}
- let(:namespaces) { table(:namespaces)}
- let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace')}
- let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id)}
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
+ let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
let(:approval_project_rule) { approval_project_rules.create!(name: 'rule', project_id: project.id, severity_levels: severity_levels) }
context 'without having all severity levels selected' do
@@ -27,7 +27,7 @@ RSpec.describe ResetSeverityLevelsToNewDefault do
it 'changes severity_levels to the default value' do
expect(approval_project_rule.severity_levels).to eq(severity_levels)
- expect { migrate! }.to change {approval_project_rule.reload.severity_levels}.from(severity_levels).to(default_levels)
+ expect { migrate! }.to change { approval_project_rule.reload.severity_levels }.from(severity_levels).to(default_levels)
end
end
end
diff --git a/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb b/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb
new file mode 100644
index 00000000000..675cc332e69
--- /dev/null
+++ b/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleBackfillClusterAgentsHasVulnerabilities do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules background jobs for each batch of cluster agents' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :cluster_agents,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 16e1d8fbc4d..b5f153e7add 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -116,12 +116,20 @@ RSpec.describe ApplicationSetting do
it { is_expected.to validate_presence_of(:max_yaml_depth) }
it { is_expected.to validate_numericality_of(:max_yaml_depth).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_pages_size) }
+ it { is_expected.to validate_presence_of(:max_pages_custom_domains_per_project) }
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
is_expected.to validate_numericality_of(:max_pages_size).only_integer.is_greater_than_or_equal_to(0)
.is_less_than(::Gitlab::Pages::MAX_SIZE / 1.megabyte)
end
+ it 'ensures max_pages_custom_domains_per_project is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
+ is_expected
+ .to validate_numericality_of(:max_pages_custom_domains_per_project)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ end
+
it { is_expected.to validate_presence_of(:jobs_per_stage_page_size) }
it { is_expected.to validate_numericality_of(:jobs_per_stage_page_size).only_integer.is_greater_than_or_equal_to(0) }
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index 737348765d9..1dd0386060d 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -13,10 +13,15 @@ RSpec.describe Ci::BuildDependencies do
status: 'success')
end
- let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
- let!(:rspec_test) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
- let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
- let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
+ let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+ let(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
+ let(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, ci_stage: build_stage) }
+ let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, ci_stage: test_stage) }
+ let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, ci_stage: deploy_stage) }
+ let!(:rspec_test) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage_idx: 1, ci_stage: test_stage)
+ end
context 'for local dependencies' do
subject { described_class.new(job).all }
@@ -63,7 +68,7 @@ RSpec.describe Ci::BuildDependencies do
name: 'dag_job',
scheduling_type: :dag,
stage_idx: 2,
- stage: 'deploy'
+ ci_stage: deploy_stage
)
end
@@ -87,7 +92,7 @@ RSpec.describe Ci::BuildDependencies do
name: 'final',
scheduling_type: scheduling_type,
stage_idx: 3,
- stage: 'deploy',
+ ci_stage: deploy_stage,
options: { dependencies: dependencies }
)
end
@@ -218,12 +223,12 @@ RSpec.describe Ci::BuildDependencies do
cross_pipeline_limit.times do |index|
create(:ci_build, :success,
pipeline: parent_pipeline, name: "dependency-#{index}",
- stage_idx: 1, stage: 'build', user: user
+ stage_idx: 1, ci_stage: build_stage, user: user
)
create(:ci_build, :success,
pipeline: sibling_pipeline, name: "dependency-#{index}",
- stage_idx: 1, stage: 'build', user: user
+ stage_idx: 1, ci_stage: build_stage, user: user
)
end
end
@@ -355,7 +360,7 @@ RSpec.describe Ci::BuildDependencies do
describe '#all' do
let!(:job) do
- create(:ci_build, pipeline: pipeline, name: 'deploy', stage_idx: 3, stage: 'deploy')
+ create(:ci_build, pipeline: pipeline, name: 'deploy', stage_idx: 3, ci_stage: deploy_stage)
end
let(:dependencies) { described_class.new(job) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index b865688d370..7ee381b29ea 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -67,31 +67,6 @@ RSpec.describe Ci::Build do
create(:ci_build)
end
-
- context 'when the execute_build_hooks_inline flag is disabled' do
- before do
- stub_feature_flags(execute_build_hooks_inline: false)
- end
-
- it 'uses the old job hooks worker' do
- expect(::BuildHooksWorker).to receive(:perform_async).with(Ci::Build)
-
- create(:ci_build)
- end
- end
-
- context 'when the execute_build_hooks_inline flag is enabled for a project' do
- before do
- stub_feature_flags(execute_build_hooks_inline: project)
- end
-
- it 'executes hooks inline' do
- expect(::BuildHooksWorker).not_to receive(:perform_async)
- expect_next(described_class).to receive(:execute_hooks)
-
- create(:ci_build, project: project)
- end
- end
end
end
@@ -594,6 +569,51 @@ RSpec.describe Ci::Build do
end
end
+ describe '#prevent_rollback_deployment?' do
+ subject { build.prevent_rollback_deployment? }
+
+ let(:build) { create(:ci_build, :created, :with_deployment, project: project, environment: 'production') }
+
+ context 'when build has no environment' do
+ let(:build) { create(:ci_build, :created, project: project, environment: nil) }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when project has forward deployment disabled' do
+ before do
+ project.ci_cd_settings.update!(forward_deployment_enabled: false)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when deployment cannot rollback' do
+ before do
+ expect(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when prevent_outdated_deployment_jobs FF is disabled' do
+ before do
+ stub_feature_flags(prevent_outdated_deployment_jobs: false)
+ expect(build.deployment).not_to receive(:rollback?)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when build can prevent rollback deployment' do
+ before do
+ expect(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ end
+
+ it { expect(subject).to be_truthy }
+ end
+ end
+
describe '#schedulable?' do
subject { build.schedulable? }
@@ -1250,70 +1270,6 @@ RSpec.describe Ci::Build do
end
end
- describe '#has_old_trace?' do
- subject { build.has_old_trace? }
-
- context 'when old trace exists' do
- before do
- build.update_column(:trace, 'old trace')
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when old trace does not exist' do
- it { is_expected.to be_falsy }
- end
- end
-
- describe '#trace=' do
- it "expect to fail trace=" do
- expect { build.trace = "new" }.to raise_error(NotImplementedError)
- end
- end
-
- describe '#old_trace' do
- subject { build.old_trace }
-
- before do
- build.update_column(:trace, 'old trace')
- end
-
- it "expect to receive data from database" do
- is_expected.to eq('old trace')
- end
- end
-
- describe '#erase_old_trace!' do
- subject { build.erase_old_trace! }
-
- context 'when old trace exists' do
- before do
- build.update_column(:trace, 'old trace')
- end
-
- it "erases old trace" do
- subject
-
- expect(build.old_trace).to be_nil
- end
-
- it "executes UPDATE query" do
- recorded = ActiveRecord::QueryRecorder.new { subject }
-
- expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(1)
- end
- end
-
- context 'when old trace does not exist' do
- it 'does not execute UPDATE query' do
- recorded = ActiveRecord::QueryRecorder.new { subject }
-
- expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(0)
- end
- end
- end
-
describe '#hide_secrets' do
let(:metrics) { spy('metrics') }
let(:subject) { build.hide_secrets(data) }
@@ -1370,13 +1326,12 @@ RSpec.describe Ci::Build do
subject { build.send(event) }
- where(:ff_enabled, :state, :report_count, :trait) do
- true | :success! | 1 | :sast
- true | :cancel! | 1 | :sast
- true | :drop! | 2 | :multiple_report_artifacts
- true | :success! | 0 | :allowed_to_fail
- true | :skip! | 0 | :pending
- false | :success! | 0 | :sast
+ where(:state, :report_count, :trait) do
+ :success! | 1 | :sast
+ :cancel! | 1 | :sast
+ :drop! | 2 | :multiple_report_artifacts
+ :success! | 0 | :allowed_to_fail
+ :skip! | 0 | :pending
end
with_them do
@@ -1386,7 +1341,6 @@ RSpec.describe Ci::Build do
context "when transitioning to #{params[:state]}" do
before do
allow(Gitlab).to receive(:com?).and_return(true)
- stub_feature_flags(report_artifact_build_completed_metrics_on_build_completion: ff_enabled)
end
it 'increments build_completed_report_type metric' do
@@ -1645,32 +1599,6 @@ RSpec.describe Ci::Build do
end
end
- describe '#count_user_verification?' do
- subject { build.count_user_verification? }
-
- context 'when build is the verify action for the environment' do
- let(:build) do
- create(:ci_build,
- ref: 'master',
- environment: 'staging',
- options: { environment: { action: 'verify' } })
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when build is not the verify action for the environment' do
- let(:build) do
- create(:ci_build,
- ref: 'master',
- environment: 'staging',
- options: { environment: { action: 'start' } })
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
describe '#expanded_environment_name' do
subject { build.expanded_environment_name }
@@ -1873,12 +1801,6 @@ RSpec.describe Ci::Build do
context 'build is not erasable' do
let!(:build) { create(:ci_build) }
- describe '#erase' do
- subject { build.erase }
-
- it { is_expected.to be false }
- end
-
describe '#erasable?' do
subject { build.erasable? }
@@ -1887,71 +1809,9 @@ RSpec.describe Ci::Build do
end
context 'build is erasable' do
- context 'logging erase' do
- let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) }
-
- it 'logs erased artifacts' do
- expect(Gitlab::Ci::Artifacts::Logger)
- .to receive(:log_deleted)
- .with(
- match_array(build.job_artifacts.to_a),
- 'Ci::Build#erase'
- )
-
- build.erase
- end
- end
-
- context 'when project is undergoing stats refresh' do
- let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) }
-
- describe '#erase' do
- before do
- allow(build.project).to receive(:refreshing_build_artifacts_size?).and_return(true)
- end
-
- it 'logs and continues with deleting the artifacts' do
- expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_artifact_deletion_during_stats_refresh).with(
- method: 'Ci::Build#erase',
- project_id: build.project.id
- )
-
- build.erase
-
- expect(build.job_artifacts.count).to eq(0)
- end
- end
- end
-
context 'new artifacts' do
let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) }
- describe '#erase' do
- before do
- build.erase(erased_by: erased_by)
- end
-
- context 'erased by user' do
- let!(:erased_by) { create(:user, username: 'eraser') }
-
- include_examples 'erasable'
-
- it 'records user who erased a build' do
- expect(build.erased_by).to eq erased_by
- end
- end
-
- context 'erased by system' do
- let(:erased_by) { nil }
-
- include_examples 'erasable'
-
- it 'does not set user who erased a build' do
- expect(build.erased_by).to be_nil
- end
- end
- end
-
describe '#erasable?' do
subject { build.erasable? }
@@ -1969,76 +1829,12 @@ RSpec.describe Ci::Build do
context 'job has been erased' do
before do
- build.erase
+ build.update!(erased_at: 1.minute.ago)
end
it { is_expected.to be_truthy }
end
end
-
- context 'metadata and build trace are not available' do
- let!(:build) { create(:ci_build, :success, :artifacts) }
-
- before do
- build.erase_erasable_artifacts!
- end
-
- describe '#erase' do
- it 'does not raise error' do
- expect { build.erase }.not_to raise_error
- end
- end
- end
- end
- end
- end
-
- describe '#erase_erasable_artifacts!' do
- let!(:build) { create(:ci_build, :success) }
-
- subject { build.erase_erasable_artifacts! }
-
- before do
- Ci::JobArtifact.file_types.keys.each do |file_type|
- create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym])
- end
- end
-
- it "erases erasable artifacts and logs them" do
- expect(Gitlab::Ci::Artifacts::Logger)
- .to receive(:log_deleted)
- .with(
- match_array(build.job_artifacts.erasable.to_a),
- 'Ci::Build#erase_erasable_artifacts!'
- )
-
- subject
-
- expect(build.job_artifacts.erasable).to be_empty
- end
-
- it "keeps non erasable artifacts" do
- subject
-
- Ci::JobArtifact::NON_ERASABLE_FILE_TYPES.each do |file_type|
- expect(build.send("job_artifacts_#{file_type}")).not_to be_nil
- end
- end
-
- context 'when the project is undergoing stats refresh' do
- before do
- allow(build.project).to receive(:refreshing_build_artifacts_size?).and_return(true)
- end
-
- it 'logs and continues with deleting the artifacts' do
- expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_artifact_deletion_during_stats_refresh).with(
- method: 'Ci::Build#erase_erasable_artifacts!',
- project_id: build.project.id
- )
-
- subject
-
- expect(build.job_artifacts.erasable).to be_empty
end
end
end
@@ -2689,17 +2485,17 @@ RSpec.describe Ci::Build do
describe '#ref_slug' do
{
- 'master' => 'master',
- '1-foo' => '1-foo',
- 'fix/1-foo' => 'fix-1-foo',
- 'fix-1-foo' => 'fix-1-foo',
- 'a' * 63 => 'a' * 63,
- 'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
- '-' + 'a' * 61 + '-' => 'a' * 61,
- '-' + 'a' * 62 + '-' => 'a' * 62,
- '-' + 'a' * 63 + '-' => 'a' * 62,
- 'a' * 62 + ' ' => 'a' * 62
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ '-' + 'a' * 61 + '-' => 'a' * 61,
+ '-' + 'a' * 62 + '-' => 'a' * 62,
+ '-' + 'a' * 63 + '-' => 'a' * 62,
+ 'a' * 62 + ' ' => 'a' * 62
}.each do |ref, slug|
it "transforms #{ref} to #{slug}" do
build.ref = ref
@@ -3634,17 +3430,6 @@ RSpec.describe Ci::Build do
it 'includes deploy token variables' do
is_expected.to include(*deploy_token_variables)
end
-
- context 'when the FF ci_variable_for_group_gitlab_deploy_token is disabled' do
- before do
- stub_feature_flags(ci_variable_for_group_gitlab_deploy_token: false)
- end
-
- it 'does not include deploy token variables' do
- expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER' }).to be_nil
- expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD' }).to be_nil
- end
- end
end
end
end
@@ -3921,18 +3706,6 @@ RSpec.describe Ci::Build do
build.enqueue
end
- context 'when the execute_build_hooks_inline flag is disabled' do
- before do
- stub_feature_flags(execute_build_hooks_inline: false)
- end
-
- it 'queues BuildHooksWorker' do
- expect(BuildHooksWorker).to receive(:perform_async).with(build)
-
- build.enqueue
- end
- end
-
it 'executes hooks' do
expect(build).to receive(:execute_hooks)
@@ -4048,10 +3821,6 @@ RSpec.describe Ci::Build do
context 'when artifacts of depended job has been erased' do
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
- before do
- pre_stage_job.erase
- end
-
it { expect(job).not_to have_valid_build_dependencies }
end
end
@@ -4072,10 +3841,6 @@ RSpec.describe Ci::Build do
context 'when artifacts of depended job has been erased' do
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
- before do
- pre_stage_job.erase
- end
-
it { expect(job).to have_valid_build_dependencies }
end
end
@@ -4405,9 +4170,7 @@ RSpec.describe Ci::Build do
end
describe '#collect_test_reports!' do
- subject { build.collect_test_reports!(test_reports) }
-
- let(:test_reports) { Gitlab::Ci::Reports::TestReport.new }
+ subject(:test_reports) { build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) }
it { expect(test_reports.get_suite(build.name).total_count).to eq(0) }
@@ -4455,56 +4218,6 @@ RSpec.describe Ci::Build do
end
end
end
-
- context 'when build is part of parallel build' do
- let(:build_1) { create(:ci_build, name: 'build 1/2') }
- let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
-
- before do
- build_1.collect_test_reports!(test_report)
- end
-
- it 'uses the group name for test suite name' do
- expect(test_report.test_suites.keys).to contain_exactly('build')
- end
-
- context 'when there are more than one parallel builds' do
- let(:build_2) { create(:ci_build, name: 'build 2/2') }
-
- before do
- build_2.collect_test_reports!(test_report)
- end
-
- it 'merges the test suite from parallel builds' do
- expect(test_report.test_suites.keys).to contain_exactly('build')
- end
- end
- end
-
- context 'when build is part of matrix build' do
- let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
- let(:matrix_build_1) { create(:ci_build, :matrix) }
-
- before do
- matrix_build_1.collect_test_reports!(test_report)
- end
-
- it 'uses the job name for the test suite' do
- expect(test_report.test_suites.keys).to contain_exactly(matrix_build_1.name)
- end
-
- context 'when there are more than one matrix builds' do
- let(:matrix_build_2) { create(:ci_build, :matrix) }
-
- before do
- matrix_build_2.collect_test_reports!(test_report)
- end
-
- it 'keeps separate test suites' do
- expect(test_report.test_suites.keys).to match_array([matrix_build_1.name, matrix_build_2.name])
- end
- end
- end
end
describe '#collect_accessibility_reports!' do
@@ -5620,7 +5333,7 @@ RSpec.describe Ci::Build do
end
end
- describe '#runner_features' do
+ describe '#runtime_runner_features' do
subject do
build.save!
build.cancel_gracefully?
@@ -5701,4 +5414,28 @@ RSpec.describe Ci::Build do
end
end
end
+
+ describe '#test_suite_name' do
+ let(:build) { create(:ci_build, name: 'test') }
+
+ it 'uses the group name for test suite name' do
+ expect(build.test_suite_name).to eq('test')
+ end
+
+ context 'when build is part of parallel build' do
+ let(:build) { create(:ci_build, name: 'build 1/2') }
+
+ it 'uses the group name for test suite name' do
+ expect(build.test_suite_name).to eq('build')
+ end
+ end
+
+ context 'when build is part of matrix build' do
+ let!(:matrix_build) { create(:ci_build, :matrix) }
+
+ it 'uses the job name for the test suite' do
+ expect(matrix_build.test_suite_name).to eq(matrix_build.name)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb
index f51381f7a5f..ecbb7af64f7 100644
--- a/spec/models/ci/freeze_period_status_spec.rb
+++ b/spec/models/ci/freeze_period_status_spec.rb
@@ -59,4 +59,13 @@ RSpec.describe Ci::FreezePeriodStatus do
it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1)
end
+
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/370472
+ context 'when period overlaps with itself' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') }
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0)
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0)
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index b996bf84529..098f8bd4514 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe Ci::JobArtifact do
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:job) }
+ it { is_expected.to validate_presence_of(:job) }
+ it { is_expected.to validate_presence_of(:partition_id) }
end
it { is_expected.to respond_to(:file) }
@@ -48,82 +50,86 @@ RSpec.describe Ci::JobArtifact do
end
end
- describe '.test_reports' do
- subject { described_class.test_reports }
+ describe '.of_report_type' do
+ subject { described_class.of_report_type(report_type) }
- context 'when there is a test report' do
- let!(:artifact) { create(:ci_job_artifact, :junit) }
+ describe 'test_reports' do
+ let(:report_type) { :test }
- it { is_expected.to eq([artifact]) }
- end
+ context 'when there is a test report' do
+ let!(:artifact) { create(:ci_job_artifact, :junit) }
- context 'when there are no test reports' do
- let!(:artifact) { create(:ci_job_artifact, :archive) }
+ it { is_expected.to eq([artifact]) }
+ end
- it { is_expected.to be_empty }
+ context 'when there are no test reports' do
+ let!(:artifact) { create(:ci_job_artifact, :archive) }
+
+ it { is_expected.to be_empty }
+ end
end
- end
- describe '.accessibility_reports' do
- subject { described_class.accessibility_reports }
+ describe 'accessibility_reports' do
+ let(:report_type) { :accessibility }
- context 'when there is an accessibility report' do
- let(:artifact) { create(:ci_job_artifact, :accessibility) }
+ context 'when there is an accessibility report' do
+ let(:artifact) { create(:ci_job_artifact, :accessibility) }
- it { is_expected.to eq([artifact]) }
- end
+ it { is_expected.to eq([artifact]) }
+ end
- context 'when there are no accessibility report' do
- let(:artifact) { create(:ci_job_artifact, :archive) }
+ context 'when there are no accessibility report' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
- it { is_expected.to be_empty }
+ it { is_expected.to be_empty }
+ end
end
- end
- describe '.coverage_reports' do
- subject { described_class.coverage_reports }
+ describe 'coverage_reports' do
+ let(:report_type) { :coverage }
- context 'when there is a coverage report' do
- let!(:artifact) { create(:ci_job_artifact, :cobertura) }
+ context 'when there is a coverage report' do
+ let!(:artifact) { create(:ci_job_artifact, :cobertura) }
- it { is_expected.to eq([artifact]) }
- end
+ it { is_expected.to eq([artifact]) }
+ end
- context 'when there are no coverage reports' do
- let!(:artifact) { create(:ci_job_artifact, :archive) }
+ context 'when there are no coverage reports' do
+ let!(:artifact) { create(:ci_job_artifact, :archive) }
- it { is_expected.to be_empty }
+ it { is_expected.to be_empty }
+ end
end
- end
- describe '.codequality_reports' do
- subject { described_class.codequality_reports }
+ describe 'codequality_reports' do
+ let(:report_type) { :codequality }
- context 'when there is a codequality report' do
- let!(:artifact) { create(:ci_job_artifact, :codequality) }
+ context 'when there is a codequality report' do
+ let!(:artifact) { create(:ci_job_artifact, :codequality) }
- it { is_expected.to eq([artifact]) }
- end
+ it { is_expected.to eq([artifact]) }
+ end
- context 'when there are no codequality reports' do
- let!(:artifact) { create(:ci_job_artifact, :archive) }
+ context 'when there are no codequality reports' do
+ let!(:artifact) { create(:ci_job_artifact, :archive) }
- it { is_expected.to be_empty }
+ it { is_expected.to be_empty }
+ end
end
- end
- describe '.terraform_reports' do
- context 'when there is a terraform report' do
- it 'return the job artifact' do
- artifact = create(:ci_job_artifact, :terraform)
+ describe 'terraform_reports' do
+ let(:report_type) { :terraform }
+
+ context 'when there is a terraform report' do
+ let!(:artifact) { create(:ci_job_artifact, :terraform) }
- expect(described_class.terraform_reports).to eq([artifact])
+ it { is_expected.to eq([artifact]) }
end
- end
- context 'when there are no terraform reports' do
- it 'return the an empty array' do
- expect(described_class.terraform_reports).to eq([])
+ context 'when there are no terraform reports' do
+ let!(:artifact) { create(:ci_job_artifact, :archive) }
+
+ it { is_expected.to be_empty }
end
end
end
@@ -135,7 +141,7 @@ RSpec.describe Ci::JobArtifact do
context 'when given an unrecognized report type' do
it 'raises error' do
- expect { described_class.file_types_for_report(:blah) }.to raise_error(KeyError, /blah/)
+ expect { described_class.file_types_for_report(:blah) }.to raise_error(ArgumentError, "Unrecognized report type: blah")
end
end
end
@@ -146,8 +152,8 @@ RSpec.describe Ci::JobArtifact do
subject { Ci::JobArtifact.associated_file_types_for(file_type) }
where(:file_type, :result) do
- 'codequality' | %w(codequality)
- 'quality' | nil
+ 'codequality' | %w(codequality)
+ 'quality' | nil
end
with_them do
@@ -754,4 +760,26 @@ RSpec.describe Ci::JobArtifact do
let!(:model) { create(:ci_job_artifact, project: parent) }
end
end
+
+ describe 'partitioning' do
+ let(:job) { build(:ci_build, partition_id: 123) }
+ let(:artifact) { build(:ci_job_artifact, job: job, partition_id: nil) }
+
+ it 'copies the partition_id from job' do
+ expect { artifact.valid? }.to change(artifact, :partition_id).from(nil).to(123)
+ end
+
+ context 'when the job is missing' do
+ let(:artifact) do
+ build(:ci_job_artifact,
+ project: build_stubbed(:project),
+ job: nil,
+ partition_id: nil)
+ end
+
+ it 'does not change the partition_id value' do
+ expect { artifact.valid? }.not_to change(artifact, :partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/namespace_mirror_spec.rb b/spec/models/ci/namespace_mirror_spec.rb
index 3e77c349ccb..29447cbc89d 100644
--- a/spec/models/ci/namespace_mirror_spec.rb
+++ b/spec/models/ci/namespace_mirror_spec.rb
@@ -16,7 +16,9 @@ RSpec.describe Ci::NamespaceMirror do
expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id])
expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
- expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id, group4.id])
+ expect(group4.reload.ci_namespace_mirror).to have_attributes(
+ traversal_ids: [group1.id, group2.id, group3.id, group4.id]
+ )
end
context 'scopes' do
@@ -103,6 +105,8 @@ RSpec.describe Ci::NamespaceMirror do
describe '.sync!' do
subject(:sync) { described_class.sync!(Namespaces::SyncEvent.last) }
+ let(:expected_traversal_ids) { [group1.id, group2.id, group3.id] }
+
context 'when namespace mirror does not exist in the first place' do
let(:namespace) { group3 }
@@ -114,7 +118,7 @@ RSpec.describe Ci::NamespaceMirror do
it 'creates the mirror' do
expect { sync }.to change { described_class.count }.from(3).to(4)
- expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
+ expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: expected_traversal_ids)
end
end
@@ -128,36 +132,8 @@ RSpec.describe Ci::NamespaceMirror do
it 'updates the mirror' do
expect { sync }.not_to change { described_class.count }
- expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
- end
- end
-
- shared_context 'changing the middle namespace' do
- let(:namespace) { group2 }
-
- before do
- group2.update!(parent: nil) # creates a sync event
- end
-
- it 'updates traversal_ids for the base and descendants' do
- expect { sync }.not_to change { described_class.count }
-
- expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
- expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id])
- expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id])
- expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id, group4.id])
- end
- end
-
- it_behaves_like 'changing the middle namespace'
-
- context 'when the FFs use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do
- before do
- stub_feature_flags(use_traversal_ids: false,
- use_traversal_ids_for_ancestors: false)
+ expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: expected_traversal_ids)
end
-
- it_behaves_like 'changing the middle namespace'
end
end
end
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index b051f646bd4..3038cdc944b 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -227,6 +227,19 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
expect(subject.size).to eq(size)
expect(subject.file_format).to eq(Ci::PipelineArtifact::REPORT_TYPES[file_type].to_s)
expect(subject.expire_at).to eq(Ci::PipelineArtifact::EXPIRATION_DATE.from_now)
+ expect(subject.locked).to eq('unknown')
+ end
+
+ it "creates a new pipeline artifact with pipeline's locked state" do
+ artifact = Ci::PipelineArtifact.create_or_replace_for_pipeline!(
+ pipeline: pipeline,
+ file_type: file_type,
+ file: file,
+ size: size,
+ locked: pipeline.locked
+ )
+
+ expect(artifact.locked).to eq(pipeline.locked)
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 0c28c99c113..ec03030a4b8 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -466,6 +466,48 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe '.jobs_count_in_alive_pipelines' do
+ before do
+ ::Ci::HasStatus::ALIVE_STATUSES.each do |status|
+ alive_pipeline = create(:ci_pipeline, status: status, project: project)
+ create(:ci_build, pipeline: alive_pipeline)
+ create(:ci_bridge, pipeline: alive_pipeline)
+ end
+
+ completed_pipeline = create(:ci_pipeline, :success, project: project)
+ create(:ci_build, pipeline: completed_pipeline)
+
+ old_pipeline = create(:ci_pipeline, :running, project: project, created_at: 2.days.ago)
+ create(:ci_build, pipeline: old_pipeline)
+ end
+
+ it 'includes all jobs in alive pipelines created in the last 24 hours' do
+ expect(described_class.jobs_count_in_alive_pipelines)
+ .to eq(::Ci::HasStatus::ALIVE_STATUSES.count * 2)
+ end
+ end
+
+ describe '.builds_count_in_alive_pipelines' do
+ before do
+ ::Ci::HasStatus::ALIVE_STATUSES.each do |status|
+ alive_pipeline = create(:ci_pipeline, status: status, project: project)
+ create(:ci_build, pipeline: alive_pipeline)
+ create(:ci_bridge, pipeline: alive_pipeline)
+ end
+
+ completed_pipeline = create(:ci_pipeline, :success, project: project)
+ create(:ci_build, pipeline: completed_pipeline)
+
+ old_pipeline = create(:ci_pipeline, :running, project: project, created_at: 2.days.ago)
+ create(:ci_build, pipeline: old_pipeline)
+ end
+
+ it 'includes all builds in alive pipelines created in the last 24 hours' do
+ expect(described_class.builds_count_in_alive_pipelines)
+ .to eq(::Ci::HasStatus::ALIVE_STATUSES.count)
+ end
+ end
+
describe '#merge_request?' do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be_with_reload(:pipeline) do
@@ -686,7 +728,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
describe '.with_reports' do
context 'when pipeline has a test report' do
- subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
+ subject { described_class.with_reports(Ci::JobArtifact.of_report_type(:test)) }
let!(:pipeline_with_report) { create(:ci_pipeline, :with_test_reports) }
@@ -696,7 +738,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
context 'when pipeline has a coverage report' do
- subject { described_class.with_reports(Ci::JobArtifact.coverage_reports) }
+ subject { described_class.with_reports(Ci::JobArtifact.of_report_type(:coverage)) }
let!(:pipeline_with_report) { create(:ci_pipeline, :with_coverage_reports) }
@@ -706,7 +748,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
context 'when pipeline has an accessibility report' do
- subject { described_class.with_reports(Ci::JobArtifact.accessibility_reports) }
+ subject { described_class.with_reports(Ci::JobArtifact.of_report_type(:accessibility)) }
let(:pipeline_with_report) { create(:ci_pipeline, :with_accessibility_reports) }
@@ -716,7 +758,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
context 'when pipeline has a codequality report' do
- subject { described_class.with_reports(Ci::JobArtifact.codequality_reports) }
+ subject { described_class.with_reports(Ci::JobArtifact.of_report_type(:codequality)) }
let(:pipeline_with_report) { create(:ci_pipeline, :with_codequality_reports) }
@@ -729,14 +771,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it 'selects the pipeline' do
pipeline_with_report = create(:ci_pipeline, :with_terraform_reports)
- expect(described_class.with_reports(Ci::JobArtifact.terraform_reports)).to eq(
+ expect(described_class.with_reports(Ci::JobArtifact.of_report_type(:terraform))).to eq(
[pipeline_with_report]
)
end
end
context 'when pipeline does not have metrics reports' do
- subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
+ subject { described_class.with_reports(Ci::JobArtifact.of_report_type(:test)) }
let!(:pipeline_without_report) { create(:ci_empty_pipeline) }
@@ -1375,32 +1417,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let(:pipeline) { build(:ci_empty_pipeline, :created) }
before do
- create(:ci_stage, project: project,
- pipeline: pipeline,
- position: 4,
- name: 'deploy')
-
- create(:ci_build, project: project,
- pipeline: pipeline,
- stage: 'test',
- stage_idx: 3,
- name: 'test')
-
- create(:ci_build, project: project,
- pipeline: pipeline,
- stage: 'build',
- stage_idx: 2,
- name: 'build')
-
- create(:ci_stage, project: project,
- pipeline: pipeline,
- position: 1,
- name: 'sanity')
-
- create(:ci_stage, project: project,
- pipeline: pipeline,
- position: 5,
- name: 'cleanup')
+ create(:ci_stage, project: project, pipeline: pipeline, position: 4, name: 'deploy')
+ create(:ci_build, project: project, pipeline: pipeline, stage: 'test', stage_idx: 3, name: 'test')
+ create(:ci_build, project: project, pipeline: pipeline, stage: 'build', stage_idx: 2, name: 'build')
+ create(:ci_stage, project: project, pipeline: pipeline, position: 1, name: 'sanity')
+ create(:ci_stage, project: project, pipeline: pipeline, position: 5, name: 'cleanup')
end
subject { pipeline.stages }
@@ -1577,6 +1598,42 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe 'track artifact report' do
+ let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, status: :running, user: create(:user)) }
+
+ context 'when transitioning to completed status' do
+ %i[drop! skip! succeed! cancel!].each do |command|
+ it "performs worker on transition to #{command}" do
+ expect(Ci::JobArtifacts::TrackArtifactReportWorker).to receive(:perform_async).with(pipeline.id)
+ pipeline.send(command)
+ end
+ end
+ end
+
+ context 'when pipeline retried from failed to success', :clean_gitlab_redis_shared_state do
+ let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:start_time) { 1.week.ago }
+ let(:end_time) { 1.week.from_now }
+
+ it 'counts only one report' do
+ expect(Ci::JobArtifacts::TrackArtifactReportWorker).to receive(:perform_async).with(pipeline.id).twice.and_call_original
+
+ Sidekiq::Testing.inline! do
+ pipeline.drop!
+ pipeline.run!
+ pipeline.succeed!
+ end
+
+ unique_pipeline_pass = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: test_event_name,
+ start_date: start_time,
+ end_date: end_time
+ )
+ expect(unique_pipeline_pass).to eq(1)
+ end
+ end
+ end
+
describe 'merge request metrics' do
let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
@@ -1649,9 +1706,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
context 'when auto merge is enabled' do
let_it_be_with_reload(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
let_it_be_with_reload(:pipeline) do
- create(:ci_pipeline, :running, project: merge_request.source_project,
- ref: merge_request.source_branch,
- sha: merge_request.diff_head_sha)
+ create(:ci_pipeline, :running,
+ project: merge_request.source_project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha)
end
before_all do
@@ -3615,8 +3671,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#environments_in_self_and_descendants' do
- subject { pipeline.environments_in_self_and_descendants }
+ describe '#environments_in_self_and_project_descendants' do
+ subject { pipeline.environments_in_self_and_project_descendants }
context 'when pipeline is not child nor parent' do
let_it_be(:pipeline) { create(:ci_pipeline, :created) }
@@ -4022,13 +4078,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#self_and_descendants_complete?' do
+ describe '#self_and_project_descendants_complete?' do
let_it_be(:pipeline) { create(:ci_pipeline, :success) }
let_it_be(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) }
let_it_be_with_reload(:grandchild_pipeline) { create(:ci_pipeline, :success, child_of: child_pipeline) }
context 'when all pipelines in the hierarchy is complete' do
- it { expect(pipeline.self_and_descendants_complete?).to be(true) }
+ it { expect(pipeline.self_and_project_descendants_complete?).to be(true) }
end
context 'when a pipeline in the hierarchy is not complete' do
@@ -4036,12 +4092,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
grandchild_pipeline.update!(status: :running)
end
- it { expect(pipeline.self_and_descendants_complete?).to be(false) }
+ it { expect(pipeline.self_and_project_descendants_complete?).to be(false) }
end
end
- describe '#builds_in_self_and_descendants' do
- subject(:builds) { pipeline.builds_in_self_and_descendants }
+ describe '#builds_in_self_and_project_descendants' do
+ subject(:builds) { pipeline.builds_in_self_and_project_descendants }
let(:pipeline) { create(:ci_pipeline) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
@@ -4073,7 +4129,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#build_with_artifacts_in_self_and_descendants' do
+ describe '#build_with_artifacts_in_self_and_project_descendants' do
let_it_be(:pipeline) { create(:ci_pipeline) }
let!(:build) { create(:ci_build, name: 'test', pipeline: pipeline) }
@@ -4081,14 +4137,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let!(:child_build) { create(:ci_build, :artifacts, name: 'test', pipeline: child_pipeline) }
it 'returns the build with a given name, having artifacts' do
- expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(child_build)
+ expect(pipeline.build_with_artifacts_in_self_and_project_descendants('test')).to eq(child_build)
end
context 'when same job name is present in both parent and child pipeline' do
let!(:build) { create(:ci_build, :artifacts, name: 'test', pipeline: pipeline) }
it 'returns the job in the parent pipeline' do
- expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(build)
+ expect(pipeline.build_with_artifacts_in_self_and_project_descendants('test')).to eq(build)
end
end
end
@@ -4158,7 +4214,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
test_build = create(:ci_build, :test_reports, pipeline: pipeline)
create(:ci_build, :coverage_reports, pipeline: pipeline)
- expect(pipeline.latest_report_builds(Ci::JobArtifact.test_reports)).to contain_exactly(test_build)
+ expect(pipeline.latest_report_builds(Ci::JobArtifact.of_report_type(:test))).to contain_exactly(test_build)
end
it 'only returns not retried builds' do
@@ -4169,7 +4225,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#latest_report_builds_in_self_and_descendants' do
+ describe '#latest_report_builds_in_self_and_project_descendants' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
let_it_be(:grandchild_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
@@ -4179,26 +4235,57 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
child_build = create(:ci_build, :coverage_reports, pipeline: child_pipeline)
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
- expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(parent_build, child_build, grandchild_build)
+ expect(pipeline.latest_report_builds_in_self_and_project_descendants).to contain_exactly(parent_build, child_build, grandchild_build)
end
it 'filters builds by scope' do
create(:ci_build, :test_reports, pipeline: pipeline)
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
- expect(pipeline.latest_report_builds_in_self_and_descendants(Ci::JobArtifact.codequality_reports)).to contain_exactly(grandchild_build)
+ expect(pipeline.latest_report_builds_in_self_and_project_descendants(Ci::JobArtifact.of_report_type(:codequality))).to contain_exactly(grandchild_build)
end
it 'only returns builds that are not retried' do
create(:ci_build, :codequality_reports, :retried, pipeline: grandchild_pipeline)
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
- expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(grandchild_build)
+ expect(pipeline.latest_report_builds_in_self_and_project_descendants).to contain_exactly(grandchild_build)
end
end
describe '#has_reports?' do
- subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) }
+ subject { pipeline.has_reports?(Ci::JobArtifact.of_report_type(:test)) }
+
+ let(:pipeline) { create(:ci_pipeline, :running) }
+
+ context 'when pipeline has builds with test reports' do
+ before do
+ create(:ci_build, :test_reports, pipeline: pipeline)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when pipeline does not have builds with test reports' do
+ before do
+ create(:ci_build, :artifacts, pipeline: pipeline)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when retried build has test reports but latest one has none' do
+ before do
+ create(:ci_build, :retried, :test_reports, pipeline: pipeline)
+ create(:ci_build, :artifacts, pipeline: pipeline)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#complete_and_has_reports?' do
+ subject { pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)) }
context 'when pipeline has builds with test reports' do
before do
@@ -4370,6 +4457,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
create(:ci_job_artifact, :junit_with_ant, job: build_java)
end
+ it 'has a test suite for each job' do
+ expect(subject.test_suites.keys).to contain_exactly('rspec', 'java')
+ end
+
it 'returns test reports with collected data' do
expect(subject.total_count).to be(7)
expect(subject.success_count).to be(5)
@@ -4388,6 +4479,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ context 'when the pipeline has parallel builds with test reports' do
+ let!(:parallel_build_1) { create(:ci_build, name: 'build 1/2', pipeline: pipeline) }
+ let!(:parallel_build_2) { create(:ci_build, name: 'build 2/2', pipeline: pipeline) }
+
+ before do
+ create(:ci_job_artifact, :junit, job: parallel_build_1)
+ create(:ci_job_artifact, :junit, job: parallel_build_2)
+ end
+
+ it 'merges the test suite from parallel builds' do
+ expect(subject.test_suites.keys).to contain_exactly('build')
+ end
+ end
+
+ context 'the pipeline has matrix builds with test reports' do
+ let!(:matrix_build_1) { create(:ci_build, :matrix, pipeline: pipeline) }
+ let!(:matrix_build_2) { create(:ci_build, :matrix, pipeline: pipeline) }
+
+ before do
+ create(:ci_job_artifact, :junit, job: matrix_build_1)
+ create(:ci_job_artifact, :junit, job: matrix_build_2)
+ end
+
+ it 'keeps separate test suites for each matrix build' do
+ expect(subject.test_suites.keys).to contain_exactly(matrix_build_1.name, matrix_build_2.name)
+ end
+ end
+
context 'when pipeline does not have any builds with test reports' do
it 'returns empty test reports' do
expect(subject.total_count).to be(0)
@@ -4472,10 +4591,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let_it_be(:pipeline) { create(:ci_pipeline) }
context 'when the scheduling type is `dag`' do
- it 'returns true' do
- create(:ci_build, pipeline: pipeline, scheduling_type: :dag)
+ context 'when the processable is a bridge' do
+ it 'returns true' do
+ create(:ci_bridge, pipeline: pipeline, scheduling_type: :dag)
+
+ expect(pipeline.uses_needs?).to eq(true)
+ end
+ end
- expect(pipeline.uses_needs?).to eq(true)
+ context 'when the processable is a build' do
+ it 'returns true' do
+ create(:ci_build, pipeline: pipeline, scheduling_type: :dag)
+
+ expect(pipeline.uses_needs?).to eq(true)
+ end
end
end
@@ -4911,8 +5040,58 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#self_and_ancestors' do
- subject(:self_and_ancestors) { pipeline.self_and_ancestors }
+ describe '#self_and_downstreams' do
+ subject(:self_and_downstreams) { pipeline.self_and_downstreams }
+
+ let(:pipeline) { create(:ci_pipeline, :created) }
+
+ context 'when pipeline is not child nor parent' do
+ it 'returns just the pipeline itself' do
+ expect(self_and_downstreams).to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when pipeline is child' do
+ let(:parent) { create(:ci_pipeline) }
+ let!(:pipeline) { create(:ci_pipeline, child_of: parent) }
+
+ it 'returns self and no ancestors' do
+ expect(self_and_downstreams).to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when pipeline is parent' do
+ let(:child) { create(:ci_pipeline, child_of: pipeline) }
+
+ it 'returns self and child' do
+ expect(self_and_downstreams).to contain_exactly(pipeline, child)
+ end
+ end
+
+ context 'when pipeline is a grandparent pipeline' do
+ let(:child) { create(:ci_pipeline, child_of: pipeline) }
+ let(:grandchild) { create(:ci_pipeline, child_of: child) }
+
+ it 'returns self, child, and grandchild' do
+ expect(self_and_downstreams).to contain_exactly(pipeline, child, grandchild)
+ end
+ end
+
+ context 'when pipeline is a triggered pipeline from a different project' do
+ let(:downstream) { create(:ci_pipeline) }
+
+ before do
+ create_source_pipeline(pipeline, downstream)
+ end
+
+ it 'returns self and cross-project downstream' do
+ expect(self_and_downstreams).to contain_exactly(pipeline, downstream)
+ end
+ end
+ end
+
+ describe '#self_and_project_ancestors' do
+ subject(:self_and_project_ancestors) { pipeline.self_and_project_ancestors }
context 'when pipeline is child' do
let(:pipeline) { create(:ci_pipeline, :created) }
@@ -4925,7 +5104,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
it 'returns parent and self' do
- expect(self_and_ancestors).to contain_exactly(parent, pipeline)
+ expect(self_and_project_ancestors).to contain_exactly(parent, pipeline)
end
end
@@ -4939,7 +5118,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
it 'returns only self' do
- expect(self_and_ancestors).to contain_exactly(pipeline)
+ expect(self_and_project_ancestors).to contain_exactly(pipeline)
+ end
+ end
+ end
+
+ describe '#complete_hierarchy_count' do
+ context 'with a combination of ancestor, descendant and sibling pipelines' do
+ let!(:pipeline) { create(:ci_pipeline) }
+ let!(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
+ let!(:sibling_pipeline) { create(:ci_pipeline, child_of: pipeline) }
+ let!(:grandchild_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
+
+ it 'counts the whole tree' do
+ expect(sibling_pipeline.complete_hierarchy_count).to eq(4)
end
end
end
@@ -5165,16 +5357,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to be_falsey }
end
- context 'when the pipeline is still running' do
- let(:pipeline) { create(:ci_pipeline, :running) }
+ context 'when the pipeline is still running and with test reports' do
+ let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports) }
- it { is_expected.to be_falsey }
- end
-
- context 'when the pipeline is completed without test reports' do
- let(:pipeline) { create(:ci_pipeline, :success) }
-
- it { is_expected.to be_falsey }
+ it { is_expected.to be_truthy }
end
end
@@ -5298,4 +5484,36 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
+
+ describe 'partitioning' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ before do
+ allow(described_class).to receive(:current_partition_value) { 123 }
+ end
+
+ it 'sets partition_id to the current partition value' do
+ expect { pipeline.valid? }.to change(pipeline, :partition_id).to(123)
+ end
+
+ context 'when it is already set' do
+ let(:pipeline) { build(:ci_pipeline, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { pipeline.valid? }.not_to change(pipeline, :partition_id)
+ end
+ end
+
+ context 'without current partition value' do
+ before do
+ allow(described_class).to receive(:current_partition_value) {}
+ end
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { pipeline.valid? }.not_to change(pipeline, :partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb
index 4e8d49585d0..fdcec0e96af 100644
--- a/spec/models/ci/pipeline_variable_spec.rb
+++ b/spec/models/ci/pipeline_variable_spec.rb
@@ -17,4 +17,25 @@ RSpec.describe Ci::PipelineVariable do
it { is_expected.to be_a(Hash) }
it { is_expected.to eq({ key: 'foo', value: 'bar' }) }
end
+
+ describe 'partitioning' do
+ context 'with pipeline' do
+ let(:pipeline) { build(:ci_pipeline, partition_id: 123) }
+ let(:variable) { build(:ci_pipeline_variable, pipeline: pipeline, partition_id: nil) }
+
+ it 'copies the partition_id from pipeline' do
+ expect { variable.valid? }.to change(variable, :partition_id).from(nil).to(123)
+ end
+ end
+
+ context 'without pipeline' do
+ subject(:variable) { build(:ci_pipeline_variable, pipeline: nil, partition_id: nil) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { variable.valid? }.not_to change(variable, :partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 127a1417d9e..61e2864a518 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -30,10 +30,8 @@ RSpec.describe Ci::Processable do
let_it_be(:downstream_project) { create(:project, :repository) }
let_it_be_with_refind(:processable) do
- create(
- :ci_bridge, :success, pipeline: pipeline, downstream: downstream_project,
- description: 'a trigger job', stage_id: stage.id
- )
+ create(:ci_bridge, :success,
+ pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', stage_id: stage.id)
end
let(:clone_accessors) { ::Ci::Bridge.clone_accessors }
@@ -57,8 +55,7 @@ RSpec.describe Ci::Processable do
let(:clone_accessors) { ::Ci::Build.clone_accessors.without(::Ci::Build.extra_accessors) }
let(:reject_accessors) do
- %i[id status user token_encrypted coverage trace runner
- artifacts_expire_at
+ %i[id status user token_encrypted coverage runner artifacts_expire_at
created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
@@ -86,7 +83,7 @@ RSpec.describe Ci::Processable do
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
queuing_entry runtime_metadata trace_metadata
- dast_site_profile dast_scanner_profile].freeze
+ dast_site_profile dast_scanner_profile stage_id].freeze
end
before_all do
@@ -208,10 +205,11 @@ RSpec.describe Ci::Processable do
let(:environment_name) { 'review/$CI_COMMIT_REF_SLUG-$GITLAB_USER_ID' }
let!(:processable) do
- create(:ci_build, :with_deployment, environment: environment_name,
- options: { environment: { name: environment_name } },
- pipeline: pipeline, stage_id: stage.id, project: project,
- user: other_developer)
+ create(:ci_build, :with_deployment,
+ environment: environment_name,
+ options: { environment: { name: environment_name } },
+ pipeline: pipeline, stage_id: stage.id, project: project,
+ user: other_developer)
end
it 're-uses the previous persisted environment' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ae8748f8ae3..181351222c1 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -266,13 +266,13 @@ RSpec.describe Ci::Runner do
end
shared_examples '.belonging_to_parent_group_of_project' do
- let!(:group1) { create(:group) }
- let!(:project1) { create(:project, group: group1) }
- let!(:runner1) { create(:ci_runner, :group, groups: [group1]) }
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:project1) { create(:project, group: group1) }
+ let_it_be(:runner1) { create(:ci_runner, :group, groups: [group1]) }
- let!(:group2) { create(:group) }
- let!(:project2) { create(:project, group: group2) }
- let!(:runner2) { create(:ci_runner, :group, groups: [group2]) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:project2) { create(:project, group: group2) }
+ let_it_be(:runner2) { create(:ci_runner, :group, groups: [group2]) }
let(:project_id) { project1.id }
@@ -495,8 +495,8 @@ RSpec.describe Ci::Runner do
describe '.active' do
subject { described_class.active(active_value) }
- let!(:runner1) { create(:ci_runner, :instance, active: false) }
- let!(:runner2) { create(:ci_runner, :instance) }
+ let_it_be(:runner1) { create(:ci_runner, :instance, active: false) }
+ let_it_be(:runner2) { create(:ci_runner, :instance) }
context 'with active_value set to false' do
let(:active_value) { false }
@@ -544,7 +544,7 @@ RSpec.describe Ci::Runner do
end
describe '#stale?', :clean_gitlab_redis_cache do
- let(:runner) { create(:ci_runner, :instance) }
+ let(:runner) { build(:ci_runner, :instance) }
subject { runner.stale? }
@@ -619,7 +619,7 @@ RSpec.describe Ci::Runner do
end
describe '#online?', :clean_gitlab_redis_cache do
- let(:runner) { create(:ci_runner, :instance) }
+ let(:runner) { build(:ci_runner, :instance) }
subject { runner.online? }
@@ -1016,7 +1016,7 @@ RSpec.describe Ci::Runner do
let!(:last_update) { runner.ensure_runner_queue_value }
before do
- Ci::Runners::UpdateRunnerService.new(runner).update(description: 'new runner') # rubocop: disable Rails/SaveBang
+ Ci::Runners::UpdateRunnerService.new(runner).execute(description: 'new runner')
end
it 'sets a new last_update value' do
@@ -1162,13 +1162,13 @@ RSpec.describe Ci::Runner do
end
describe '.assignable_for' do
- let(:project) { create(:project) }
- let(:group) { create(:group) }
- let(:another_project) { create(:project) }
- let!(:unlocked_project_runner) { create(:ci_runner, :project, projects: [project]) }
- let!(:locked_project_runner) { create(:ci_runner, :project, locked: true, projects: [project]) }
- let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
- let!(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:unlocked_project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:locked_project_runner) { create(:ci_runner, :project, locked: true, projects: [project]) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
context 'with already assigned project' do
subject { described_class.assignable_for(project) }
@@ -1186,59 +1186,74 @@ RSpec.describe Ci::Runner do
end
end
- describe "belongs_to_one_project?" do
- it "returns false if there are two projects runner assigned to" do
- project1 = create(:project)
- project2 = create(:project)
- runner = create(:ci_runner, :project, projects: [project1, project2])
+ context 'Project-related queries' do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+
+ describe '#owner_project' do
+ subject(:owner_project) { project_runner.owner_project }
+
+ context 'with project1 as first project associated with runner' do
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1, project2]) }
+
+ it { is_expected.to eq project1 }
+ end
+
+ context 'with project2 as first project associated with runner' do
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project2, project1]) }
- expect(runner.belongs_to_one_project?).to be_falsey
+ it { is_expected.to eq project2 }
+ end
end
- it "returns true" do
- project = create(:project)
- runner = create(:ci_runner, :project, projects: [project])
+ describe "belongs_to_one_project?" do
+ it "returns false if there are two projects runner is assigned to" do
+ runner = create(:ci_runner, :project, projects: [project1, project2])
+
+ expect(runner.belongs_to_one_project?).to be_falsey
+ end
- expect(runner.belongs_to_one_project?).to be_truthy
+ it "returns true if there is only one project runner is assigned to" do
+ runner = create(:ci_runner, :project, projects: [project1])
+
+ expect(runner.belongs_to_one_project?).to be_truthy
+ end
end
- end
- describe '#belongs_to_more_than_one_project?' do
- context 'project runner' do
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
+ describe '#belongs_to_more_than_one_project?' do
+ context 'project runner' do
+ context 'two projects assigned to runner' do
+ let(:runner) { create(:ci_runner, :project, projects: [project1, project2]) }
+
+ it 'returns true' do
+ expect(runner.belongs_to_more_than_one_project?).to be_truthy
+ end
+ end
- context 'two projects assigned to runner' do
- let(:runner) { create(:ci_runner, :project, projects: [project1, project2]) }
+ context 'one project assigned to runner' do
+ let(:runner) { create(:ci_runner, :project, projects: [project1]) }
- it 'returns true' do
- expect(runner.belongs_to_more_than_one_project?).to be_truthy
+ it 'returns false' do
+ expect(runner.belongs_to_more_than_one_project?).to be_falsey
+ end
end
end
- context 'one project assigned to runner' do
- let(:runner) { create(:ci_runner, :project, projects: [project1]) }
+ context 'group runner' do
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'returns false' do
expect(runner.belongs_to_more_than_one_project?).to be_falsey
end
end
- end
-
- context 'group runner' do
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, groups: [group]) }
-
- it 'returns false' do
- expect(runner.belongs_to_more_than_one_project?).to be_falsey
- end
- end
- context 'shared runner' do
- let(:runner) { create(:ci_runner, :instance) }
+ context 'shared runner' do
+ let(:runner) { create(:ci_runner, :instance) }
- it 'returns false' do
- expect(runner.belongs_to_more_than_one_project?).to be_falsey
+ it 'returns false' do
+ expect(runner.belongs_to_more_than_one_project?).to be_falsey
+ end
end
end
end
@@ -1299,7 +1314,7 @@ RSpec.describe Ci::Runner do
end
describe '.search' do
- let(:runner) { create(:ci_runner, token: '123abc', description: 'test runner') }
+ let_it_be(:runner) { create(:ci_runner, token: '123abc', description: 'test runner') }
it 'returns runners with a matching token' do
expect(described_class.search(runner.token)).to eq([runner])
@@ -1326,57 +1341,10 @@ RSpec.describe Ci::Runner do
end
end
- describe '#assigned_to_group?' do
- subject { runner.assigned_to_group? }
-
- context 'when project runner' do
- let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
- let(:project) { create(:project) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when shared runner' do
- let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when group runner' do
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
-
- it { is_expected.to be_truthy }
- end
- end
-
- describe '#assigned_to_project?' do
- subject { runner.assigned_to_project? }
-
- context 'when group runner' do
- let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
- let(:group) { create(:group) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when shared runner' do
- let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when project runner' do
- let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
- let(:project) { create(:project) }
-
- it { is_expected.to be_truthy }
- end
- end
-
describe '#pick_build!' do
+ let_it_be(:runner) { create(:ci_runner) }
+
let(:build) { create(:ci_build) }
- let(:runner) { create(:ci_runner) }
context 'runner can pick the build' do
it 'calls #tick_runner_queue' do
@@ -1413,26 +1381,26 @@ RSpec.describe Ci::Runner do
end
describe '.order_by' do
+ let_it_be(:runner1) { create(:ci_runner, created_at: 1.year.ago, contacted_at: 1.year.ago) }
+ let_it_be(:runner2) { create(:ci_runner, created_at: 1.month.ago, contacted_at: 1.month.ago) }
+
+ before do
+ runner1.update!(token_expires_at: 1.year.from_now)
+ end
+
it 'supports ordering by the contact date' do
- runner1 = create(:ci_runner, contacted_at: 1.year.ago)
- runner2 = create(:ci_runner, contacted_at: 1.month.ago)
runners = described_class.order_by('contacted_asc')
expect(runners).to eq([runner1, runner2])
end
it 'supports ordering by the creation date' do
- runner1 = create(:ci_runner, created_at: 1.year.ago)
- runner2 = create(:ci_runner, created_at: 1.month.ago)
runners = described_class.order_by('created_asc')
expect(runners).to eq([runner2, runner1])
end
it 'supports ordering by the token expiration' do
- runner1 = create(:ci_runner)
- runner1.update!(token_expires_at: 1.year.from_now)
- runner2 = create(:ci_runner)
runner3 = create(:ci_runner)
runner3.update!(token_expires_at: 1.month.from_now)
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index d55a8509a98..dd9af33a562 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -369,4 +369,33 @@ RSpec.describe Ci::Stage, :models do
let!(:model) { create(:ci_stage, project: parent) }
end
end
+
+ describe 'partitioning' do
+ context 'with pipeline' do
+ let(:pipeline) { build(:ci_pipeline, partition_id: 123) }
+ let(:stage) { build(:ci_stage, pipeline: pipeline) }
+
+ it 'copies the partition_id from pipeline' do
+ expect { stage.valid? }.to change(stage, :partition_id).to(123)
+ end
+
+ context 'when it is already set' do
+ let(:stage) { build(:ci_stage, pipeline: pipeline, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { stage.valid? }.not_to change(stage, :partition_id)
+ end
+ end
+ end
+
+ context 'without pipeline' do
+ subject(:stage) { build(:ci_stage, pipeline: nil) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { stage.valid? }.not_to change(stage, :partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 4ac8720780c..8517e583ec7 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe Ci::Trigger do
trigger = create(:ci_trigger_without_token, project: project)
expect(trigger.token).not_to be_nil
+ expect(trigger.token).to start_with(Ci::Trigger::TRIGGER_TOKEN_PREFIX)
end
it 'does not set a random token if one provided' do
@@ -30,12 +31,22 @@ RSpec.describe Ci::Trigger do
end
describe '#short_token' do
- let(:trigger) { create(:ci_trigger, token: '12345678') }
+ let(:trigger) { create(:ci_trigger) }
subject { trigger.short_token }
- it 'returns shortened token' do
- is_expected.to eq('1234')
+ it 'returns shortened token without prefix' do
+ is_expected.not_to start_with(Ci::Trigger::TRIGGER_TOKEN_PREFIX)
+ end
+
+ context 'token does not have a prefix' do
+ before do
+ trigger.token = '12345678'
+ end
+
+ it 'returns shortened token' do
+ is_expected.to eq('1234')
+ end
end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index a0ede9fb0d9..7b9ff409edd 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -605,15 +605,15 @@ RSpec.describe Clusters::Platforms::Kubernetes do
{ 'app.gitlab.com/app' => project.full_path_slug, 'app.gitlab.com/env' => 'env-000000' }
])
expect(rollout_status.instances).to eq([{ pod_name: "kube-pod",
- stable: true,
- status: "pending",
- tooltip: "kube-pod (Pending)",
- track: "stable" },
+ stable: true,
+ status: "pending",
+ tooltip: "kube-pod (Pending)",
+ track: "stable" },
{ pod_name: "Not provided",
- stable: true,
- status: "pending",
- tooltip: "Not provided (Pending)",
- track: "stable" }])
+ stable: true,
+ status: "pending",
+ tooltip: "Not provided (Pending)",
+ track: "stable" }])
end
context 'with canary ingress' do
diff --git a/spec/models/commit_signatures/gpg_signature_spec.rb b/spec/models/commit_signatures/gpg_signature_spec.rb
index 6ae2a202b72..605ad725dd7 100644
--- a/spec/models/commit_signatures/gpg_signature_spec.rb
+++ b/spec/models/commit_signatures/gpg_signature_spec.rb
@@ -5,12 +5,14 @@ require 'spec_helper'
RSpec.describe CommitSignatures::GpgSignature do
# This commit is seeded from https://gitlab.com/gitlab-org/gitlab-test
# For instructions on how to add more seed data, see the project README
- let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
- let!(:project) { create(:project, :repository, path: 'sample-project') }
- let!(:commit) { create(:commit, project: project, sha: commit_sha) }
- let(:signature) { create(:gpg_signature, commit_sha: commit_sha) }
- let(:gpg_key) { create(:gpg_key) }
- let(:gpg_key_subkey) { create(:gpg_key_subkey) }
+ let_it_be(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+ let_it_be(:project) { create(:project, :repository, path: 'sample-project') }
+ let_it_be(:commit) { create(:commit, project: project, sha: commit_sha) }
+ let_it_be(:gpg_key) { create(:gpg_key) }
+ let_it_be(:gpg_key_subkey) { create(:gpg_key_subkey, gpg_key: gpg_key) }
+
+ let(:signature) { create(:gpg_signature, commit_sha: commit_sha, gpg_key: gpg_key) }
+
let(:attributes) do
{
commit_sha: commit_sha,
@@ -35,8 +37,7 @@ RSpec.describe CommitSignatures::GpgSignature do
end
describe '.by_commit_sha scope' do
- let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
- let!(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+ let_it_be(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) }
it 'returns all gpg signatures by sha' do
expect(described_class.by_commit_sha(commit_sha)).to match_array([signature])
diff --git a/spec/models/commit_signatures/ssh_signature_spec.rb b/spec/models/commit_signatures/ssh_signature_spec.rb
index 64d95fe3a71..08530bf6964 100644
--- a/spec/models/commit_signatures/ssh_signature_spec.rb
+++ b/spec/models/commit_signatures/ssh_signature_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe CommitSignatures::SshSignature do
# This commit is seeded from https://gitlab.com/gitlab-org/gitlab-test
# For instructions on how to add more seed data, see the project README
- let(:commit_sha) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' }
- let!(:project) { create(:project, :repository, path: 'sample-project') }
- let!(:commit) { create(:commit, project: project, sha: commit_sha) }
- let(:signature) { create(:ssh_signature, commit_sha: commit_sha) }
- let(:ssh_key) { create(:ed25519_key_256) }
+ let_it_be(:commit_sha) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' }
+ let_it_be(:project) { create(:project, :repository, path: 'sample-project') }
+ let_it_be(:commit) { create(:commit, project: project, sha: commit_sha) }
+ let_it_be(:ssh_key) { create(:ed25519_key_256) }
+
let(:attributes) do
{
commit_sha: commit_sha,
@@ -18,6 +18,8 @@ RSpec.describe CommitSignatures::SshSignature do
}
end
+ let(:signature) { create(:ssh_signature, commit_sha: commit_sha, key: ssh_key) }
+
it_behaves_like 'having unique enum values'
it_behaves_like 'commit signature'
diff --git a/spec/models/commit_signatures/x509_commit_signature_spec.rb b/spec/models/commit_signatures/x509_commit_signature_spec.rb
index beb101cdd89..b971fd078e2 100644
--- a/spec/models/commit_signatures/x509_commit_signature_spec.rb
+++ b/spec/models/commit_signatures/x509_commit_signature_spec.rb
@@ -5,11 +5,10 @@ require 'spec_helper'
RSpec.describe CommitSignatures::X509CommitSignature do
# This commit is seeded from https://gitlab.com/gitlab-org/gitlab-test
# For instructions on how to add more seed data, see the project README
- let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' }
- let(:project) { create(:project, :public, :repository) }
- let!(:commit) { create(:commit, project: project, sha: commit_sha) }
- let(:x509_certificate) { create(:x509_certificate) }
- let(:signature) { create(:x509_commit_signature, commit_sha: commit_sha) }
+ let_it_be(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:commit) { create(:commit, project: project, sha: commit_sha) }
+ let_it_be(:x509_certificate) { create(:x509_certificate) }
let(:attributes) do
{
@@ -20,6 +19,8 @@ RSpec.describe CommitSignatures::X509CommitSignature do
}
end
+ let(:signature) { create(:x509_commit_signature, commit_sha: commit_sha, x509_certificate: x509_certificate) }
+
it_behaves_like 'having unique enum values'
it_behaves_like 'commit signature'
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 08d770a1beb..bab6247d4f9 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -84,6 +84,22 @@ RSpec.describe Commit do
end
end
+ describe '.build_from_sidekiq_hash' do
+ it 'returns a Commit' do
+ commit = described_class.build_from_sidekiq_hash(project, id: '123')
+
+ expect(commit).to be_an_instance_of(Commit)
+ end
+
+ it 'parses date strings into Time instances' do
+ commit = described_class.build_from_sidekiq_hash(project,
+ id: '123',
+ authored_date: Time.current.to_s)
+
+ expect(commit.authored_date).to be_a_kind_of(Time)
+ end
+ end
+
describe '#diff_refs' do
it 'is equal to itself' do
expect(commit.diff_refs).to eq(commit.diff_refs)
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 78d4d9de84e..adbd20b6730 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -836,17 +836,11 @@ RSpec.describe CommitStatus do
context 'when commit status does not have stage but it exists' do
let!(:stage) do
- create(:ci_stage, project: project,
- pipeline: pipeline,
- name: 'test')
+ create(:ci_stage, project: project, pipeline: pipeline, name: 'test')
end
let(:commit_status) do
- create(:commit_status, project: project,
- pipeline: pipeline,
- name: 'rspec',
- stage: 'test',
- status: :success)
+ create(:commit_status, project: project, pipeline: pipeline, name: 'rspec', stage: 'test', status: :success)
end
it 'uses existing stage', :sidekiq_might_not_need_inline do
@@ -1008,4 +1002,53 @@ RSpec.describe CommitStatus do
let!(:model) { create(:ci_build, runner: parent) }
end
end
+
+ describe '.stage_name' do
+ subject(:stage_name) { commit_status.stage_name }
+
+ it 'returns the stage name' do
+ expect(stage_name).to eq('test')
+ end
+
+ context 'when ci stage is not present' do
+ before do
+ commit_status.ci_stage = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe 'partitioning' do
+ context 'with pipeline' do
+ let(:pipeline) { build(:ci_pipeline, partition_id: 123) }
+ let(:status) { build(:commit_status, pipeline: pipeline) }
+
+ it 'copies the partition_id from pipeline' do
+ expect { status.valid? }.to change(status, :partition_id).to(123)
+ end
+
+ context 'when it is already set' do
+ let(:status) { build(:commit_status, pipeline: pipeline, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { status.valid? }.not_to change(status, :partition_id)
+ end
+ end
+ end
+
+ context 'without pipeline' do
+ subject(:status) do
+ build(:commit_status,
+ project: build_stubbed(:project),
+ pipeline: nil)
+ end
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { status.valid? }.not_to change(status, :partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/approvable_base_spec.rb b/spec/models/concerns/approvable_spec.rb
index 2bf6a98a64d..1ddd9b3edca 100644
--- a/spec/models/concerns/approvable_base_spec.rb
+++ b/spec/models/concerns/approvable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ApprovableBase do
+RSpec.describe Approvable do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb
index 64691165e21..6af244a5a0f 100644
--- a/spec/models/concerns/ci/artifactable_spec.rb
+++ b/spec/models/concerns/ci/artifactable_spec.rb
@@ -46,30 +46,8 @@ RSpec.describe Ci::Artifactable do
end
end
- context 'when file format is zip' do
- context 'when artifact contains one file' do
- let(:artifact) { build(:ci_job_artifact, :zip_with_single_file) }
-
- it 'iterates blob once' do
- expect { |b| artifact.each_blob(&b) }.to yield_control.once
- end
- end
-
- context 'when artifact contains two files' do
- let(:artifact) { build(:ci_job_artifact, :zip_with_multiple_files) }
-
- it 'iterates blob two times' do
- expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(2).times
- end
- end
- end
-
context 'when there are no adapters for the file format' do
- let(:artifact) { build(:ci_job_artifact, :junit) }
-
- before do
- allow(artifact).to receive(:file_format).and_return(:unknown)
- end
+ let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) }
it 'raises an error' do
expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError)
diff --git a/spec/models/concerns/ci/has_deployment_name_spec.rb b/spec/models/concerns/ci/has_deployment_name_spec.rb
deleted file mode 100644
index 8c7338638b1..00000000000
--- a/spec/models/concerns/ci/has_deployment_name_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::HasDeploymentName do
- describe 'deployment_name?' do
- let(:build) { create(:ci_build) }
-
- subject { build.branch? }
-
- it 'does detect deployment names' do
- build.name = 'deployment'
-
- expect(build.deployment_name?).to be_truthy
- end
-
- it 'does detect partial deployment names' do
- build.name = 'do a really cool deploy'
-
- expect(build.deployment_name?).to be_truthy
- end
-
- it 'does not detect non-deployment names' do
- build.name = 'testing'
-
- expect(build.deployment_name?).to be_falsy
- end
-
- it 'is case insensitive' do
- build.name = 'DEPLOY'
- expect(build.deployment_name?).to be_truthy
- end
- end
-end
diff --git a/spec/models/concerns/ci/track_environment_usage_spec.rb b/spec/models/concerns/ci/track_environment_usage_spec.rb
new file mode 100644
index 00000000000..d75972c49b5
--- /dev/null
+++ b/spec/models/concerns/ci/track_environment_usage_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::TrackEnvironmentUsage do
+ describe '#verifies_environment?' do
+ subject { build.verifies_environment? }
+
+ context 'when build is the verify action for the environment' do
+ let(:build) do
+ build_stubbed(:ci_build,
+ ref: 'master',
+ environment: 'staging',
+ options: { environment: { action: 'verify' } })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when build is not the verify action for the environment' do
+ let(:build) do
+ build_stubbed(:ci_build,
+ ref: 'master',
+ environment: 'staging',
+ options: { environment: { action: 'start' } })
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe 'deployment_name?' do
+ let(:build) { create(:ci_build) }
+
+ subject { build.branch? }
+
+ it 'does detect deployment names' do
+ build.name = 'deployment'
+
+ expect(build).to be_deployment_name
+ end
+
+ it 'does detect partial deployment names' do
+ build.name = 'do a really cool deploy'
+
+ expect(build).to be_deployment_name
+ end
+
+ it 'does not detect non-deployment names' do
+ build.name = 'testing'
+
+ expect(build).not_to be_deployment_name
+ end
+
+ it 'is case insensitive' do
+ build.name = 'DEPLOY'
+
+ expect(build).to be_deployment_name
+ end
+ end
+end
diff --git a/spec/models/concerns/counter_attribute_spec.rb b/spec/models/concerns/counter_attribute_spec.rb
index 8d32ef14f47..2dd70188740 100644
--- a/spec/models/concerns/counter_attribute_spec.rb
+++ b/spec/models/concerns/counter_attribute_spec.rb
@@ -79,4 +79,14 @@ RSpec.describe CounterAttribute, :counter_attribute, :clean_gitlab_redis_shared_
end
end
end
+
+ describe '.counter_attribute_enabled?' do
+ it 'is true when counter attribute is defined' do
+ expect(CounterAttributeModel.counter_attribute_enabled?(:build_artifacts_size)).to be_truthy
+ end
+
+ it 'is false when counter attribute is not defined' do
+ expect(CounterAttributeModel.counter_attribute_enabled?(:nope)).to be_falsey
+ end
+ end
end
diff --git a/spec/models/concerns/from_set_operator_spec.rb b/spec/models/concerns/from_set_operator_spec.rb
index 8ebbb5550c9..1ef0d1adb08 100644
--- a/spec/models/concerns/from_set_operator_spec.rb
+++ b/spec/models/concerns/from_set_operator_spec.rb
@@ -3,7 +3,22 @@
require 'spec_helper'
RSpec.describe FromSetOperator do
- describe 'when set operator method already exists' do
+ let_it_be(:from_set_operator) do
+ Class.new do
+ extend FromSetOperator
+ define_set_operator Gitlab::SQL::Union
+
+ def table_name
+ 'groups'
+ end
+
+ def from(*args)
+ ''
+ end
+ end
+ end
+
+ context 'when set operator method already exists' do
let(:redefine_method) do
Class.new do
def self.from_union
@@ -17,4 +32,38 @@ RSpec.describe FromSetOperator do
it { expect { redefine_method }.to raise_exception(RuntimeError) }
end
+
+ context 'with members' do
+ let_it_be(:group1) { create :group }
+ let_it_be(:group2) { create :group }
+ let_it_be(:groups) do
+ [
+ Group.where(id: group1),
+ Group.where(id: group2)
+ ]
+ end
+
+ shared_examples 'set operator called with correct members' do
+ it do
+ expect(Gitlab::SQL::Union).to receive(:new).with(groups, anything).and_call_original
+ subject
+ end
+ end
+
+ context 'as array' do
+ subject { from_set_operator.new.from_union(groups) }
+
+ it_behaves_like 'set operator called with correct members'
+
+ it { expect { subject }.not_to make_queries }
+ end
+
+ context 'as multiple parameters' do
+ subject { from_set_operator.new.from_union(*groups) }
+
+ it_behaves_like 'set operator called with correct members'
+
+ it { expect { subject }.not_to make_queries }
+ end
+ end
end
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 55e3caf3c4c..3e42a3504ac 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -96,6 +96,7 @@ RSpec.describe PgFullTextSearchable do
it 'ignores accents' do
expect(model_class.pg_full_text_search('jurgen')).to contain_exactly(with_accent)
+ expect(model_class.pg_full_text_search('Jürgen')).to contain_exactly(with_accent)
end
it 'does not support searching by non-Latin characters' do
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index b49b9ce8a2a..89f34834aa4 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe ProjectFeaturesCompatibility do
let(:features) do
features_enabled + %w(
repository pages operations container_registry package_registry environments feature_flags releases
+ monitor
)
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index cb9bb676ede..039b9e574fe 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -281,7 +281,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it 'does not delete the value key' do
- expect(Rails.cache).to receive(:delete).with(cache_key).never
+ expect(Rails.cache).not_to receive(:delete).with(cache_key)
go!
end
@@ -338,7 +338,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'when lifetime is exceeded' do
it 'skips the calculation' do
- expect(instance).to receive(:calculate_reactive_cache).never
+ expect(instance).not_to receive(:calculate_reactive_cache)
go!
end
@@ -354,7 +354,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it 'skips the calculation' do
stub_exclusive_lease_taken(cache_key)
- expect(instance).to receive(:calculate_reactive_cache).never
+ expect(instance).not_to receive(:calculate_reactive_cache)
go!
end
diff --git a/spec/models/container_registry/event_spec.rb b/spec/models/container_registry/event_spec.rb
index 799d9d4fd1c..c2c494c49fb 100644
--- a/spec/models/container_registry/event_spec.rb
+++ b/spec/models/container_registry/event_spec.rb
@@ -144,7 +144,7 @@ RSpec.describe ContainerRegistry::Event do
let(:target) do
{
'mediaType' => ContainerRegistry::Client::DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE,
- 'repository' => repository_path,
+ 'repository' => repository_path,
'tag' => 'latest'
}
end
diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb
index 1833fcf5385..d19a0bdf6c7 100644
--- a/spec/models/customer_relations/organization_spec.rb
+++ b/spec/models/customer_relations/organization_spec.rb
@@ -146,15 +146,43 @@ RSpec.describe CustomerRelations::Organization, type: :model do
end
end
- describe '.sort_by_name' do
- let_it_be(:organization_a) { create(:organization, group: group, name: "c") }
+ describe '.counts_by_state' do
+ before do
+ create_list(:organization, 3, group: group)
+ create_list(:organization, 2, group: group, state: 'inactive')
+ end
+
+ it 'returns correct organization counts' do
+ counts = group.organizations.counts_by_state
+
+ expect(counts['active']).to be(3)
+ expect(counts['inactive']).to be(2)
+ end
+
+ it 'returns 0 with no results' do
+ counts = group.organizations.where(id: non_existing_record_id).counts_by_state
+
+ expect(counts['active']).to be(0)
+ expect(counts['inactive']).to be(0)
+ end
+ end
+
+ describe 'sorting' do
+ let_it_be(:organization_a) { create(:organization, group: group, name: "c", description: "1") }
let_it_be(:organization_b) { create(:organization, group: group, name: "a") }
- let_it_be(:organization_c) { create(:organization, group: group, name: "b") }
+ let_it_be(:organization_c) { create(:organization, group: group, name: "b", description: "2") }
- context 'when sorting the organizations' do
+ describe '.sort_by_name' do
it 'sorts them by name in ascendent order' do
expect(group.organizations.sort_by_name).to eq([organization_b, organization_c, organization_a])
end
end
+
+ describe '.sort_by_field' do
+ it 'sorts them by description in descending order' do
+ expect(group.organizations.sort_by_field('description', :desc))
+ .to eq([organization_c, organization_a, organization_b])
+ end
+ end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 0a4ee73f3d3..87fa5289795 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -74,6 +74,27 @@ RSpec.describe Deployment do
end
end
+ describe '.for_iid' do
+ subject { described_class.for_iid(project, iid) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:deployment) { create(:deployment, project: project) }
+
+ let(:iid) { deployment.iid }
+
+ it 'finds the deployment' do
+ is_expected.to contain_exactly(deployment)
+ end
+
+ context 'when iid does not match' do
+ let(:iid) { non_existing_record_id }
+
+ it 'does not find the deployment' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.for_environment_name' do
subject { described_class.for_environment_name(project, environment_name) }
@@ -142,8 +163,8 @@ RSpec.describe Deployment do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(deployment_id: deployment.id, status: 'running',
- status_changed_at: Time.current)
+ .to receive(:perform_async)
+ .with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
deployment.run!
end
@@ -179,8 +200,8 @@ RSpec.describe Deployment do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(deployment_id: deployment.id, status: 'success',
- status_changed_at: Time.current)
+ .to receive(:perform_async)
+ .with(deployment_id: deployment.id, status: 'success', status_changed_at: Time.current)
deployment.succeed!
end
@@ -209,8 +230,8 @@ RSpec.describe Deployment do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(deployment_id: deployment.id, status: 'failed',
- status_changed_at: Time.current)
+ .to receive(:perform_async)
+ .with(deployment_id: deployment.id, status: 'failed', status_changed_at: Time.current)
deployment.drop!
end
@@ -239,8 +260,8 @@ RSpec.describe Deployment do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(deployment_id: deployment.id, status: 'canceled',
- status_changed_at: Time.current)
+ .to receive(:perform_async)
+ .with(deployment_id: deployment.id, status: 'canceled', status_changed_at: Time.current)
deployment.cancel!
end
@@ -343,6 +364,31 @@ RSpec.describe Deployment do
end
end
+ describe '#older_than_last_successful_deployment?' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+
+ subject { deployment.older_than_last_successful_deployment? }
+
+ context 'when deployment is current deployment' do
+ let(:deployment) { create(:deployment, :success, project: project) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when deployment is behind current deployment' do
+ let!(:deployment) do
+ create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
+ end
+
+ let!(:last_deployment) do
+ create(:deployment, :success, project: project, environment: environment)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe '#success?' do
subject { deployment.success? }
@@ -517,6 +563,16 @@ RSpec.describe Deployment do
end
end
+ describe '.ordered_as_upcoming' do
+ let!(:deployment1) { create(:deployment, status: :running) }
+ let!(:deployment2) { create(:deployment, status: :blocked) }
+ let!(:deployment3) { create(:deployment, status: :created) }
+
+ it 'sorts by ID DESC' do
+ expect(described_class.ordered_as_upcoming).to eq([deployment3, deployment2, deployment1])
+ end
+ end
+
describe 'visible' do
subject { described_class.visible }
@@ -876,6 +932,22 @@ RSpec.describe Deployment do
end
end
+ describe '#build' do
+ let!(:deployment) { create(:deployment) }
+
+ subject { deployment.build }
+
+ it 'retrieves build for the deployment' do
+ is_expected.to eq(deployment.deployable)
+ end
+
+ it 'returns nil when the associated build is not found' do
+ deployment.update!(deployable_id: nil, deployable_type: nil)
+
+ is_expected.to be_nil
+ end
+ end
+
describe '#previous_deployment' do
using RSpec::Parameterized::TableSyntax
@@ -1233,6 +1305,19 @@ RSpec.describe Deployment do
end
end
+ describe '#tags' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:deployment) { create(:deployment, project: project) }
+
+ subject { deployment.tags }
+
+ it 'will return tags related to this deployment' do
+ expect(project.repository).to receive(:tag_names_contains).with(deployment.sha, limit: 100).and_return(['test'])
+
+ is_expected.to match_array(['test'])
+ end
+ end
+
describe '#valid_sha' do
it 'does not add errors for a valid SHA' do
project = create(:project, :repository)
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
index 519ba3c67b4..44ecae82174 100644
--- a/spec/models/design_management/version_spec.rb
+++ b/spec/models/design_management/version_spec.rb
@@ -256,7 +256,7 @@ RSpec.describe DesignManagement::Version do
it 'puts them in the right buckets' do
expect(version.designs_by_event).to match(
a_hash_including(
- 'creation' => have_attributes(size: 3),
+ 'creation' => have_attributes(size: 3),
'modification' => have_attributes(size: 4),
'deletion' => have_attributes(size: 5)
)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 3f4372dafd0..1e15b09a069 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -17,6 +17,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it { is_expected.to nullify_if_blank(:external_url) }
it { is_expected.to belong_to(:project).required }
+ it { is_expected.to belong_to(:merge_request).optional }
+
it { is_expected.to have_many(:deployments) }
it { is_expected.to have_many(:metrics_dashboard_annotations) }
it { is_expected.to have_many(:alert_management_alerts) }
@@ -40,6 +42,26 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(environment).to be_valid
end
+
+ context 'does not allow changes to merge_request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'for an environment that has no merge request associated' do
+ environment = create(:environment)
+
+ environment.merge_request = merge_request
+
+ expect(environment).not_to be_valid
+ end
+
+ it 'for an environment that has a merge request associated' do
+ environment = create(:environment, merge_request: merge_request)
+
+ environment.merge_request = nil
+
+ expect(environment).not_to be_valid
+ end
+ end
end
describe 'validate and sanitize external url' do
@@ -318,6 +340,16 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ describe '.for_type' do
+ it 'filters by type' do
+ create(:environment)
+ create(:environment, name: 'type1/prod')
+ env = create(:environment, name: 'type2/prod')
+
+ expect(described_class.for_type('type2')).to contain_exactly(env)
+ end
+ end
+
describe '#guess_tier' do
using RSpec::Parameterized::TableSyntax
@@ -938,6 +970,26 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ describe 'Last deployment relations' do
+ Deployment::FINISHED_STATUSES.each do |status|
+ it "returns the last #{status} deployment" do
+ create(:deployment, status.to_sym, environment: environment, finished_at: 1.day.ago)
+ expected = create(:deployment, status.to_sym, environment: environment, finished_at: Time.current)
+
+ expect(environment.public_send(:"last_#{status}_deployment")).to eq(expected)
+ end
+ end
+
+ Deployment::UPCOMING_STATUSES.each do |status|
+ it "returns the last #{status} deployment" do
+ create(:deployment, status.to_sym, environment: environment)
+ expected = create(:deployment, status.to_sym, environment: environment)
+
+ expect(environment.public_send(:"last_#{status}_deployment")).to eq(expected)
+ end
+ end
+ end
+
describe '#last_deployable' do
subject { environment.last_deployable }
@@ -1573,11 +1625,38 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(environment.auto_stop_in).to eq(expected_result)
else
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ an_instance_of(expected_result),
+ project_id: environment.project_id,
+ environment_id: environment.id
+ )
+
expect { subject }.to raise_error(expected_result)
end
end
end
end
+
+ context 'resets earlier value' do
+ let(:environment) { create(:environment, auto_stop_at: 1.day.since.round) }
+
+ where(:value, :expected_result) do
+ '2 days' | 2.days.to_i
+ '1 week' | 1.week.to_i
+ '2h20min' | 2.hours.to_i + 20.minutes.to_i
+ '' | nil
+ 'never' | nil
+ end
+ with_them do
+ it 'assigns new value' do
+ freeze_time do
+ subject
+
+ expect(environment.auto_stop_in).to eq(expected_result)
+ end
+ end
+ end
+ end
end
describe '.for_id_and_slug' do
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 0685144dea6..30e73d84cfb 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -187,9 +187,38 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
+ describe '#reactive_cache_limit_enabled?' do
+ subject { setting.reactive_cache_limit_enabled? }
+
+ it { is_expected.to eq(true) }
+
+ context 'when feature flag disabled' do
+ before do
+ stub_feature_flags(error_tracking_sentry_limit: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
describe '#sentry_client' do
- it 'returns sentry client' do
- expect(subject.sentry_client).to be_a(ErrorTracking::SentryClient)
+ subject { setting.sentry_client }
+
+ it { is_expected.to be_a(ErrorTracking::SentryClient) }
+ it { is_expected.to have_attributes(url: setting.api_url, token: setting.token) }
+
+ describe '#validate_size_guarded_by_feature_flag?' do
+ subject { setting.sentry_client.validate_size_guarded_by_feature_flag? }
+
+ it { is_expected.to eq(true) }
+
+ context 'when feature flag disabled' do
+ before do
+ stub_feature_flags(error_tracking_sentry_limit: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
end
@@ -222,70 +251,39 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
- context 'when sentry client raises ErrorTracking::SentryClient::Error' do
- before do
- synchronous_reactive_cache(subject)
-
- allow(subject).to receive(:sentry_client).and_return(sentry_client)
- allow(sentry_client).to receive(:list_issues).with(opts)
- .and_raise(ErrorTracking::SentryClient::Error, 'error message')
- end
-
- it 'returns error' do
- expect(result).to eq(
- error: 'error message',
- error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
- )
- end
- end
-
- context 'when sentry client raises ErrorTracking::SentryClient::MissingKeysError' do
- before do
- synchronous_reactive_cache(subject)
-
- allow(subject).to receive(:sentry_client).and_return(sentry_client)
- allow(sentry_client).to receive(:list_issues).with(opts)
- .and_raise(ErrorTracking::SentryClient::MissingKeysError,
- 'Sentry API response is missing keys. key not found: "id"')
- end
-
- it 'returns error' do
- expect(result).to eq(
- error: 'Sentry API response is missing keys. key not found: "id"',
- error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
- )
- end
- end
+ describe 'client errors' do
+ using RSpec::Parameterized::TableSyntax
- context 'when sentry client raises ErrorTracking::SentryClient::ResponseInvalidSizeError' do
- let(:error_msg) { "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}." }
+ sc = ErrorTracking::SentryClient
+ pets = described_class
+ msg = 'something'
before do
synchronous_reactive_cache(subject)
allow(subject).to receive(:sentry_client).and_return(sentry_client)
- allow(sentry_client).to receive(:list_issues).with(opts)
- .and_raise(ErrorTracking::SentryClient::ResponseInvalidSizeError, error_msg)
end
- it 'returns error' do
- expect(result).to eq(
- error: error_msg,
- error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_INVALID_SIZE
- )
+ where(:exception, :error_type, :error_message) do
+ sc::Error | pets::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE | msg
+ sc::MissingKeysError | pets::SENTRY_API_ERROR_TYPE_MISSING_KEYS | msg
+ sc::ResponseInvalidSizeError | pets::SENTRY_API_ERROR_INVALID_SIZE | msg
+ sc::BadRequestError | pets::SENTRY_API_ERROR_TYPE_BAD_REQUEST | msg
+ StandardError | nil | 'Unexpected Error'
end
- end
- context 'when sentry client raises StandardError' do
- before do
- synchronous_reactive_cache(subject)
+ with_them do
+ it 'returns an error' do
+ allow(sentry_client).to receive(:list_issues).with(opts)
+ .and_raise(exception, msg)
- allow(subject).to receive(:sentry_client).and_return(sentry_client)
- allow(sentry_client).to receive(:list_issues).with(opts).and_raise(StandardError)
- end
+ expected_result = {
+ error: error_message,
+ error_type: error_type
+ }.compact
- it 'returns error' do
- expect(result).to eq(error: 'Unexpected Error')
+ expect(result).to eq(expected_result)
+ end
end
end
end
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
index 969987c7e64..eec8fe0ef71 100644
--- a/spec/models/group_group_link_spec.rb
+++ b/spec/models/group_group_link_spec.rb
@@ -44,6 +44,17 @@ RSpec.describe GroupGroupLink do
end
end
+ describe '.with_owner_access' do
+ let_it_be(:group_group_link_maintainer) { create :group_group_link, :maintainer }
+ let_it_be(:group_group_link_owner) { create :group_group_link, :owner }
+ let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
+ let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
+
+ it 'returns all records which have OWNER access' do
+ expect(described_class.with_owner_access).to match_array([group_group_link_owner])
+ end
+ end
+
context 'for access via group shares' do
let_it_be(:shared_with_group_1) { create(:group) }
let_it_be(:shared_with_group_2) { create(:group) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 61662411ac8..2ce75fb1290 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -707,7 +707,8 @@ RSpec.describe Group do
end
describe '.public_or_visible_to_user' do
- let!(:private_group) { create(:group, :private) }
+ let!(:private_group) { create(:group, :private) }
+ let!(:private_subgroup) { create(:group, :private, parent: private_group) }
let!(:internal_group) { create(:group, :internal) }
subject { described_class.public_or_visible_to_user(user) }
@@ -731,6 +732,10 @@ RSpec.describe Group do
end
it { is_expected.to match_array([private_group, internal_group, group]) }
+
+ it 'does not have access to subgroups (see accessible_to_user scope)' do
+ is_expected.not_to include(private_subgroup)
+ end
end
context 'when user is a member of private subgroup' do
@@ -839,6 +844,36 @@ RSpec.describe Group do
expect(described_class.by_ids_or_paths([new_group.id], [group_path])).to match_array([group, new_group])
end
end
+
+ describe 'accessible_to_user' do
+ subject { described_class.accessible_to_user(user) }
+
+ let_it_be(:public_group) { create(:group, :public) }
+ let_it_be(:unaccessible_group) { create(:group, :private) }
+ let_it_be(:unaccessible_subgroup) { create(:group, :private, parent: unaccessible_group) }
+ let_it_be(:accessible_group) { create(:group, :private) }
+ let_it_be(:accessible_subgroup) { create(:group, :private, parent: accessible_group) }
+
+ context 'when user is nil' do
+ let(:user) { nil }
+
+ it { is_expected.to match_array([group, public_group]) }
+ end
+
+ context 'when user is present' do
+ let(:user) { create(:user) }
+
+ it { is_expected.to match_array([group, internal_group, public_group]) }
+
+ context 'when user has access to accessible group' do
+ before do
+ accessible_group.add_developer(user)
+ end
+
+ it { is_expected.to match_array([group, internal_group, public_group, accessible_group, accessible_subgroup]) }
+ end
+ end
+ end
end
describe '#to_reference' do
@@ -1857,56 +1892,31 @@ RSpec.describe Group do
end
end
- describe '#update_two_factor_requirement' do
- let(:user) { create(:user) }
+ describe '#update_two_factor_requirement_for_members' do
+ let_it_be_with_reload(:user) { create(:user) }
context 'group membership' do
- before do
+ it 'enables two_factor_requirement for group members' do
group.add_member(user, GroupMember::OWNER)
- end
-
- it 'is called when require_two_factor_authentication is changed' do
- expect_any_instance_of(User).to receive(:update_two_factor_requirement)
-
group.update!(require_two_factor_authentication: true)
- end
-
- it 'is called when two_factor_grace_period is changed' do
- expect_any_instance_of(User).to receive(:update_two_factor_requirement)
-
- group.update!(two_factor_grace_period: 23)
- end
- it 'is not called when other attributes are changed' do
- expect_any_instance_of(User).not_to receive(:update_two_factor_requirement)
+ group.update_two_factor_requirement_for_members
- group.update!(description: 'foobar')
+ expect(user.reload.require_two_factor_authentication_from_group).to be_truthy
end
- it 'calls #update_two_factor_requirement on each group member' do
- other_user = create(:user)
- group.add_member(other_user, GroupMember::OWNER)
-
- calls = 0
- allow_any_instance_of(User).to receive(:update_two_factor_requirement) do
- calls += 1
- end
+ it 'disables two_factor_requirement for group members' do
+ user.update!(require_two_factor_authentication_from_group: true)
+ group.add_member(user, GroupMember::OWNER)
+ group.update!(require_two_factor_authentication: false)
- group.update!(require_two_factor_authentication: true, two_factor_grace_period: 23)
+ group.update_two_factor_requirement_for_members
- expect(calls).to eq 2
+ expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
context 'sub groups and projects' do
- it 'enables two_factor_requirement for group member' do
- group.add_member(user, GroupMember::OWNER)
-
- group.update!(require_two_factor_authentication: true)
-
- expect(user.reload.require_two_factor_authentication_from_group).to be_truthy
- end
-
context 'expanded group members' do
let(:indirect_user) { create(:user) }
@@ -1915,9 +1925,10 @@ RSpec.describe Group do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_member(indirect_user, GroupMember::OWNER)
-
group.update!(require_two_factor_authentication: true)
+ group.update_two_factor_requirement_for_members
+
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
@@ -1926,9 +1937,10 @@ RSpec.describe Group do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group, require_two_factor_authentication: true)
subgroup.add_member(indirect_user, GroupMember::OWNER)
-
group.update!(require_two_factor_authentication: false)
+ group.update_two_factor_requirement_for_members
+
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
@@ -1936,9 +1948,10 @@ RSpec.describe Group do
ancestor_group = create(:group)
ancestor_group.add_member(indirect_user, GroupMember::OWNER)
group.update!(parent: ancestor_group)
-
group.update!(require_two_factor_authentication: true)
+ group.update_two_factor_requirement_for_members
+
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
@@ -1949,9 +1962,10 @@ RSpec.describe Group do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_member(indirect_user, GroupMember::OWNER)
-
group.update!(require_two_factor_authentication: true)
+ group.update_two_factor_requirement_for_members
+
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
@@ -1960,9 +1974,10 @@ RSpec.describe Group do
it 'disables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_member(indirect_user, GroupMember::OWNER)
-
group.update!(require_two_factor_authentication: false)
+ group.update_two_factor_requirement_for_members
+
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey
end
@@ -1970,9 +1985,10 @@ RSpec.describe Group do
ancestor_group = create(:group, require_two_factor_authentication: false)
indirect_user.update!(require_two_factor_authentication_from_group: true)
ancestor_group.add_member(indirect_user, GroupMember::OWNER)
-
group.update!(require_two_factor_authentication: false)
+ group.update_two_factor_requirement_for_members
+
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
@@ -1983,9 +1999,10 @@ RSpec.describe Group do
it 'does not enable two_factor_requirement for child project member' do
project = create(:project, group: group)
project.add_maintainer(user)
-
group.update!(require_two_factor_authentication: true)
+ group.update_two_factor_requirement_for_members
+
expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
end
@@ -1993,15 +2010,36 @@ RSpec.describe Group do
subgroup = create(:group, :nested, parent: group)
project = create(:project, group: subgroup)
project.add_maintainer(user)
-
group.update!(require_two_factor_authentication: true)
+ group.update_two_factor_requirement_for_members
+
expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
end
end
+ describe '#update_two_factor_requirement' do
+ it 'enqueues a job when require_two_factor_authentication is changed' do
+ expect(Groups::UpdateTwoFactorRequirementForMembersWorker).to receive(:perform_async).with(group.id)
+
+ group.update!(require_two_factor_authentication: true)
+ end
+
+ it 'enqueues a job when two_factor_grace_period is changed' do
+ expect(Groups::UpdateTwoFactorRequirementForMembersWorker).to receive(:perform_async).with(group.id)
+
+ group.update!(two_factor_grace_period: 23)
+ end
+
+ it 'does not enqueue a job when other attributes are changed' do
+ expect(Groups::UpdateTwoFactorRequirementForMembersWorker).not_to receive(:perform_async).with(group.id)
+
+ group.update!(description: 'foobar')
+ end
+ end
+
describe '#path_changed_hook' do
let(:system_hook_service) { SystemHooksService.new }
@@ -2043,201 +2081,6 @@ RSpec.describe Group do
end
end
- describe '#ci_variables_for' do
- let(:project) { create(:project, group: group) }
- let(:environment_scope) { '*' }
-
- let!(:ci_variable) do
- create(:ci_group_variable, value: 'secret', group: group, environment_scope: environment_scope)
- end
-
- let!(:protected_variable) do
- create(:ci_group_variable, :protected, value: 'protected', group: group)
- end
-
- subject { group.ci_variables_for('ref', project) }
-
- it 'memoizes the result by ref and environment', :request_store do
- scoped_variable = create(:ci_group_variable, value: 'secret', group: group, environment_scope: 'scoped')
-
- expect(project).to receive(:protected_for?).with('ref').once.and_return(true)
- expect(project).to receive(:protected_for?).with('other').twice.and_return(false)
-
- 2.times do
- expect(group.ci_variables_for('ref', project, environment: 'production')).to contain_exactly(ci_variable, protected_variable)
- expect(group.ci_variables_for('other', project)).to contain_exactly(ci_variable)
- expect(group.ci_variables_for('other', project, environment: 'scoped')).to contain_exactly(ci_variable, scoped_variable)
- end
- end
-
- shared_examples 'ref is protected' do
- it 'contains all the variables' do
- is_expected.to contain_exactly(ci_variable, protected_variable)
- end
- end
-
- context 'when the ref is not protected' do
- before do
- stub_application_setting(
- default_branch_protection: Gitlab::Access::PROTECTION_NONE)
- end
-
- it 'contains only the CI variables' do
- is_expected.to contain_exactly(ci_variable)
- end
- end
-
- context 'when the ref is a protected branch' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(true)
- end
-
- it_behaves_like 'ref is protected'
- end
-
- context 'when the ref is a protected tag' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(true)
- end
-
- it_behaves_like 'ref is protected'
- end
-
- context 'when environment name is specified' do
- let(:environment) { 'review/name' }
-
- subject do
- group.ci_variables_for('ref', project, environment: environment)
- end
-
- context 'when environment scope is exactly matched' do
- let(:environment_scope) { 'review/name' }
-
- it { is_expected.to contain_exactly(ci_variable) }
- end
-
- context 'when environment scope is matched by wildcard' do
- let(:environment_scope) { 'review/*' }
-
- it { is_expected.to contain_exactly(ci_variable) }
- end
-
- context 'when environment scope does not match' do
- let(:environment_scope) { 'review/*/special' }
-
- it { is_expected.not_to contain_exactly(ci_variable) }
- end
-
- context 'when environment scope has _' do
- let(:environment_scope) { '*_*' }
-
- it 'does not treat it as wildcard' do
- is_expected.not_to contain_exactly(ci_variable)
- end
-
- context 'when environment name contains underscore' do
- let(:environment) { 'foo_bar/test' }
- let(:environment_scope) { 'foo_bar/*' }
-
- it 'matches literally for _' do
- is_expected.to contain_exactly(ci_variable)
- end
- end
- end
-
- # The environment name and scope cannot have % at the moment,
- # but we're considering relaxing it and we should also make sure
- # it doesn't break in case some data sneaked in somehow as we're
- # not checking this integrity in database level.
- context 'when environment scope has %' do
- it 'does not treat it as wildcard' do
- ci_variable.update_attribute(:environment_scope, '*%*')
-
- is_expected.not_to contain_exactly(ci_variable)
- end
-
- context 'when environment name contains a percent' do
- let(:environment) { 'foo%bar/test' }
-
- it 'matches literally for %' do
- ci_variable.update_attribute(:environment_scope, 'foo%bar/*')
-
- is_expected.to contain_exactly(ci_variable)
- end
- end
- end
-
- context 'when variables with the same name have different environment scopes' do
- let!(:partially_matched_variable) do
- create(:ci_group_variable,
- key: ci_variable.key,
- value: 'partial',
- environment_scope: 'review/*',
- group: group)
- end
-
- let!(:perfectly_matched_variable) do
- create(:ci_group_variable,
- key: ci_variable.key,
- value: 'prefect',
- environment_scope: 'review/name',
- group: group)
- end
-
- it 'puts variables matching environment scope more in the end' do
- is_expected.to eq(
- [ci_variable,
- partially_matched_variable,
- perfectly_matched_variable])
- end
- end
- end
-
- context 'when group has children' do
- let(:group_child) { create(:group, parent: group) }
- let(:group_child_2) { create(:group, parent: group_child) }
- let(:group_child_3) { create(:group, parent: group_child_2) }
- let(:variable_child) { create(:ci_group_variable, group: group_child) }
- let(:variable_child_2) { create(:ci_group_variable, group: group_child_2) }
- let(:variable_child_3) { create(:ci_group_variable, group: group_child_3) }
-
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(true)
- end
-
- context 'traversal queries' do
- shared_examples 'correct ancestor order' do
- it 'returns all variables belong to the group and parent groups' do
- expected_array1 = [protected_variable, ci_variable]
- expected_array2 = [variable_child, variable_child_2, variable_child_3]
- got_array = group_child_3.ci_variables_for('ref', project).to_a
-
- expect(got_array.shift(2)).to contain_exactly(*expected_array1)
- expect(got_array).to eq(expected_array2)
- end
- end
-
- context 'recursive' do
- before do
- stub_feature_flags(use_traversal_ids: false)
- end
-
- include_examples 'correct ancestor order'
- end
-
- context 'linear' do
- before do
- stub_feature_flags(use_traversal_ids: true)
-
- group_child_3.reload # make sure traversal_ids are reloaded
- end
-
- include_examples 'correct ancestor order'
- end
- end
- end
- end
-
describe '#highest_group_member' do
let(:nested_group) { create(:group, parent: group) }
let(:nested_group_2) { create(:group, parent: nested_group) }
@@ -2331,8 +2174,7 @@ RSpec.describe Group do
let(:another_shared_with_group) { create(:group, parent: group) }
before do
- create(:group_group_link, shared_group: nested_group,
- shared_with_group: another_shared_with_group)
+ create(:group_group_link, shared_group: nested_group, shared_with_group: another_shared_with_group)
end
it 'returns all shared with group ids' do
@@ -3465,6 +3307,23 @@ RSpec.describe Group do
end
end
+ describe '#packages_policy_subject' do
+ it 'returns wrapper' do
+ expect(group.packages_policy_subject).to be_a(Packages::Policies::Group)
+ expect(group.packages_policy_subject.group).to eq(group)
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(read_package_policy_rule: false)
+ end
+
+ it 'returns group' do
+ expect(group.packages_policy_subject).to eq(group)
+ end
+ end
+ end
+
describe '#gitlab_deploy_token' do
subject(:gitlab_deploy_token) { group.gitlab_deploy_token }
diff --git a/spec/models/incident_management/timeline_event_spec.rb b/spec/models/incident_management/timeline_event_spec.rb
index 9f4011fe6a7..fea391acda3 100644
--- a/spec/models/incident_management/timeline_event_spec.rb
+++ b/spec/models/incident_management/timeline_event_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe IncidentManagement::TimelineEvent do
it { is_expected.to validate_length_of(:action).is_at_most(128) }
end
- describe '.order_occurred_at_asc' do
+ describe '.order_occurred_at_asc_id_asc' do
let_it_be(:occurred_3mins_ago) do
create(:incident_management_timeline_event, project: project, occurred_at: 3.minutes.ago)
end
@@ -38,11 +38,23 @@ RSpec.describe IncidentManagement::TimelineEvent do
create(:incident_management_timeline_event, project: project, occurred_at: 2.minutes.ago)
end
- subject(:order) { described_class.order_occurred_at_asc }
+ subject(:order) { described_class.order_occurred_at_asc_id_asc }
it 'sorts timeline events by occurred_at' do
is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, timeline_event])
end
+
+ context 'when two events occured at the same time' do
+ let_it_be(:also_occurred_2mins_ago) do
+ create(:incident_management_timeline_event, project: project, occurred_at: occurred_2mins_ago.occurred_at)
+ end
+
+ it 'sorts timeline events by occurred_at then sorts by id' do
+ occurred_2mins_ago.touch # Interact with record of earlier id to switch default DB ordering
+
+ is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, also_occurred_2mins_ago, timeline_event])
+ end
+ end
end
describe '#cache_markdown_field' do
diff --git a/spec/models/integrations/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb
index 68ef0ccb2e4..a63cc0b6d83 100644
--- a/spec/models/integrations/chat_message/pipeline_message_spec.rb
+++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb
@@ -49,8 +49,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url])
allow(Project).to receive(:find) { test_project }
- test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors,
- yaml_errors: "yaml error description here")
+ test_pipeline = double("A test pipeline",
+ has_yaml_errors?: has_yaml_errors, yaml_errors: "yaml error description here")
allow(Ci::Pipeline).to receive(:find) { test_pipeline }
allow(Gitlab::UrlBuilder).to receive(:build).with(test_commit).and_return("http://example.com/commit")
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index cfc44b22a84..4ac684e8ff0 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -203,13 +203,10 @@ RSpec.describe Integrations::Datadog do
end
before do
- stub_feature_flags(datadog_integration_logs_collection: enable_logs_collection)
stub_request(:post, expected_hook_url)
saved_instance.execute(data)
end
- let(:enable_logs_collection) { true }
-
context 'with pipeline data' do
let(:data) { pipeline_data }
let(:expected_headers) { { ::Gitlab::WebHooks::GITLAB_EVENT_HEADER => 'Pipeline Hook' } }
@@ -232,12 +229,6 @@ RSpec.describe Integrations::Datadog do
let(:expected_body) { data.to_json }
it { expect(a_request(:post, expected_hook_url).with(headers: expected_headers, body: expected_body)).to have_been_made }
-
- context 'but feature flag disabled' do
- let(:enable_logs_collection) { false }
-
- it { expect(a_request(:post, expected_hook_url)).not_to have_been_made }
- end
end
end
end
diff --git a/spec/models/integrations/discord_spec.rb b/spec/models/integrations/discord_spec.rb
index b85620782c1..eb90acc73be 100644
--- a/spec/models/integrations/discord_spec.rb
+++ b/spec/models/integrations/discord_spec.rb
@@ -23,10 +23,10 @@ RSpec.describe Integrations::Discord do
describe '#execute' do
include StubRequests
+ let_it_be(:project) { create(:project, :repository) }
+
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
let(:webhook_url) { "https://example.gitlab.com/" }
-
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb
index 8a51f8a0705..905fee075ad 100644
--- a/spec/models/integrations/drone_ci_spec.rb
+++ b/spec/models/integrations/drone_ci_spec.rb
@@ -175,9 +175,9 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
end
{
- "killed" => :canceled,
+ "killed" => :canceled,
"failure" => :failed,
- "error" => :failed,
+ "error" => :failed,
"success" => "success"
}.each do |drone_status, our_status|
it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do
diff --git a/spec/models/integrations/hangouts_chat_spec.rb b/spec/models/integrations/hangouts_chat_spec.rb
index 17b40c484f5..828bcdf5d8f 100644
--- a/spec/models/integrations/hangouts_chat_spec.rb
+++ b/spec/models/integrations/hangouts_chat_spec.rb
@@ -12,4 +12,175 @@ RSpec.describe Integrations::HangoutsChat do
}
end
end
+
+ let(:chat_integration) { described_class.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+ let(:webhook_url_regex) { /\A#{webhook_url}.*/ }
+
+ describe "#execute" do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, :wiki_repo) }
+
+ before do
+ allow(chat_integration).to receive_messages(
+ project: project,
+ project_id: project.id,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url_regex)
+ end
+
+ context 'with push events' do
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ it "adds thread key for push events" do
+ expect(chat_integration.execute(push_sample_data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /push .*?/ }))
+ .once
+ end
+ end
+
+ context 'with issue events' do
+ let(:issues_sample_data) { create(:issue).to_hook_data(user) }
+
+ it "adds thread key for issue events" do
+ expect(chat_integration.execute(issues_sample_data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /issue .*?/ }))
+ .once
+ end
+ end
+
+ context 'with merge events' do
+ let(:merge_sample_data) { create(:merge_request).to_hook_data(user) }
+
+ it "adds thread key for merge events" do
+ expect(chat_integration.execute(merge_sample_data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /merge request .*?/ }))
+ .once
+ end
+ end
+
+ context 'with wiki page events' do
+ let(:wiki_page_sample_data) do
+ Gitlab::DataBuilder::WikiPage.build(create(:wiki_page, message: 'foo'), user, 'create')
+ end
+
+ it "adds thread key for wiki page events" do
+ expect(chat_integration.execute(wiki_page_sample_data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /wiki_page .*?/ }))
+ .once
+ end
+ end
+
+ context 'with pipeline events' do
+ let(:pipeline) do
+ create(:ci_pipeline, :failed, project: project, sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ let(:pipeline_sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+
+ it "adds thread key for pipeline events" do
+ expect(chat_integration.execute(pipeline_sample_data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /pipeline .*?/ }))
+ .once
+ end
+ end
+ end
+
+ describe "Note events" do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ allow(chat_integration).to receive_messages(
+ project: project,
+ project_id: project.id,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url_regex)
+ end
+
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "adds thread key" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+
+ expect(chat_integration.execute(data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /commit .*?/ }))
+ .once
+ end
+ end
+
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
+
+ it "adds thread key" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+
+ expect(chat_integration.execute(data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /merge request .*?/ }))
+ .once
+ end
+ end
+
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "adds thread key" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(chat_integration.execute(data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /issue .*?/ }))
+ .once
+ end
+ end
+
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
+
+ it "adds thread key" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+
+ expect(chat_integration.execute(data)).to be(true)
+
+ expect(WebMock).to have_requested(:post, webhook_url)
+ .with(query: hash_including({ "threadKey" => /snippet .*?/ }))
+ .once
+ end
+ end
+ end
end
diff --git a/spec/models/integrations/harbor_spec.rb b/spec/models/integrations/harbor_spec.rb
index 3952495119a..26b43fa3313 100644
--- a/spec/models/integrations/harbor_spec.rb
+++ b/spec/models/integrations/harbor_spec.rb
@@ -27,6 +27,12 @@ RSpec.describe Integrations::Harbor do
it { is_expected.to allow_value('https://demo.goharbor.io').for(:url) }
end
+ describe 'hostname' do
+ it 'returns the host of the integration url' do
+ expect(harbor_integration.hostname).to eq('demo.goharbor.io')
+ end
+ end
+
describe '#fields' do
it 'returns custom fields' do
expect(harbor_integration.fields.pluck(:name)).to eq(%w[url project_name username password])
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index af4c48775ec..17c3cd17364 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -150,8 +150,8 @@ RSpec.describe Issue do
issue.confidential = false
expect(issue).not_to be_valid
- expect(issue.errors[:confidential])
- .to include('associated parent is confidential and can not have non-confidential children.')
+ expect(issue.errors[:base])
+ .to include(_('A non-confidential issue cannot have a confidential parent.'))
end
it 'allows to make parent not-confidential' do
@@ -172,8 +172,8 @@ RSpec.describe Issue do
issue.confidential = true
expect(issue).not_to be_valid
- expect(issue.errors[:confidential])
- .to include('confidential parent can not be used if there are non-confidential children.')
+ expect(issue.errors[:base])
+ .to include(_('A confidential issue cannot have a parent that already has non-confidential children.'))
end
it 'allows to make child confidential' do
@@ -283,6 +283,14 @@ RSpec.describe Issue do
create(:issue)
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CREATED }
+ let(:user) { issue.author }
+ subject(:service_action) { issue }
+ end
end
context 'issue namespace' do
@@ -1782,20 +1790,4 @@ RSpec.describe Issue do
end
end
end
-
- describe '#full_search' do
- context 'when searching non-english terms' do
- [
- 'abc 中文語',
- '中文語cn',
- '中文語'
- ].each do |term|
- it 'adds extra where clause to match partial index' do
- expect(described_class.full_search(term).to_sql).to include(
- "AND (issues.title NOT SIMILAR TO '[\\u0000-\\u218F]*' OR issues.description NOT SIMILAR TO '[\\u0000-\\u218F]*')"
- )
- end
- end
- end
- end
end
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
index 3d1095845aa..9c1f7c678a9 100644
--- a/spec/models/jira_connect_installation_spec.rb
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -45,4 +45,30 @@ RSpec.describe JiraConnectInstallation do
expect(subject).to contain_exactly(subscription.installation)
end
end
+
+ describe '#oauth_authorization_url' do
+ let_it_be(:installation) { create(:jira_connect_installation) }
+
+ subject { installation.oauth_authorization_url }
+
+ before do
+ allow(Gitlab).to receive_message_chain('config.gitlab.url') { 'http://test.host' }
+ end
+
+ it { is_expected.to eq('http://test.host') }
+
+ context 'with instance_url' do
+ let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
+
+ it { is_expected.to eq('https://gitlab.example.com') }
+
+ context 'and jira_connect_oauth_self_managed feature is disabled' do
+ before do
+ stub_feature_flags(jira_connect_oauth_self_managed: false)
+ end
+
+ it { is_expected.to eq('http://test.host') }
+ end
+ end
+ end
end
diff --git a/spec/models/loose_foreign_keys/deleted_record_spec.rb b/spec/models/loose_foreign_keys/deleted_record_spec.rb
index 9ee5b7340f3..a909252a78c 100644
--- a/spec/models/loose_foreign_keys/deleted_record_spec.rb
+++ b/spec/models/loose_foreign_keys/deleted_record_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
let(:partition_manager) { Gitlab::Database::Partitioning::PartitionManager.new(described_class) }
describe 'next_partition_if callback' do
- let(:active_partition) { described_class.partitioning_strategy.active_partition.value }
+ let(:active_partition) { described_class.partitioning_strategy.active_partition }
subject(:value) { described_class.partitioning_strategy.next_partition_if.call(active_partition) }
@@ -98,7 +98,7 @@ RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
end
describe 'detach_partition_if callback' do
- let(:active_partition) { described_class.partitioning_strategy.active_partition.value }
+ let(:active_partition) { described_class.partitioning_strategy.active_partition }
subject(:value) { described_class.partitioning_strategy.detach_partition_if.call(active_partition) }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2716244b7f3..7b75a6ee1c2 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -195,6 +195,15 @@ RSpec.describe Member do
expect(member).not_to be_valid
end
end
+
+ context 'access_level cannot be changed' do
+ it 'is invalid' do
+ member.access_level = Gitlab::Access::MAINTAINER
+
+ expect(member).not_to be_valid
+ expect(member.errors.full_messages).to include( "Access level cannot be changed since member is associated with a custom role")
+ end
+ end
end
end
end
@@ -880,7 +889,8 @@ RSpec.describe Member do
end
describe 'generate invite token on create' do
- let!(:member) { build(:project_member, invite_email: "user@example.com") }
+ let(:project) { create(:project) }
+ let!(:member) { build(:project_member, invite_email: "user@example.com", project: project) }
it 'sets the invite token' do
expect { member.save! }.to change { member.invite_token }.to(kind_of(String))
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index c6266f15340..363830d21dd 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -74,7 +74,8 @@ RSpec.describe GroupMember do
describe '#update_two_factor_requirement' do
it 'is called after creation and deletion' do
user = build :user
- group_member = build :group_member, user: user
+ group = create :group
+ group_member = build :group_member, user: user, group: group
expect(user).to receive(:update_two_factor_requirement)
@@ -151,8 +152,8 @@ RSpec.describe GroupMember do
context 'when importing' do
it 'does not refresh' do
expect(UserProjectAccessChangedService).not_to receive(:new)
-
- member = build(:group_member)
+ group = create(:group)
+ member = build(:group_member, group: group)
member.importing = true
member.save!
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 99fc5dc14df..ad6f3ca5428 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -201,7 +201,7 @@ RSpec.describe ProjectMember do
it 'does not refresh' do
expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:bulk_perform_and_wait)
- member = build(:project_member)
+ member = build(:project_member, project: project)
member.importing = true
member.save!
end
diff --git a/spec/models/merge_request_assignee_spec.rb b/spec/models/merge_request_assignee_spec.rb
index 387d17d7823..73bf7d02468 100644
--- a/spec/models/merge_request_assignee_spec.rb
+++ b/spec/models/merge_request_assignee_spec.rb
@@ -38,26 +38,4 @@ RSpec.describe MergeRequestAssignee do
end
end
end
-
- it_behaves_like 'having unique enum values'
-
- describe '#attention_requested_by' do
- let(:current_user) { create(:user) }
-
- before do
- subject.update!(updated_state_by: current_user, state: :attention_requested)
- end
-
- context 'attention requested' do
- it { expect(subject.attention_requested_by).to eq(current_user) }
- end
-
- context 'attention requested' do
- before do
- subject.update!(state: :reviewed)
- end
-
- it { expect(subject.attention_requested_by).to eq(nil) }
- end
- end
end
diff --git a/spec/models/merge_request_reviewer_spec.rb b/spec/models/merge_request_reviewer_spec.rb
index 4df2dba3a7d..5a29966e4b9 100644
--- a/spec/models/merge_request_reviewer_spec.rb
+++ b/spec/models/merge_request_reviewer_spec.rb
@@ -14,24 +14,4 @@ RSpec.describe MergeRequestReviewer do
it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
it { is_expected.to belong_to(:reviewer).class_name('User').inverse_of(:merge_request_reviewers) }
end
-
- describe '#attention_requested_by' do
- let(:current_user) { create(:user) }
-
- before do
- subject.update!(updated_state_by: current_user, state: :attention_requested)
- end
-
- context 'attention requested' do
- it { expect(subject.attention_requested_by).to eq(current_user) }
- end
-
- context 'attention requested' do
- before do
- subject.update!(state: :reviewed)
- end
-
- it { expect(subject.attention_requested_by).to eq(nil) }
- end
- end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 19026a4772d..f27f3b749b1 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
it { is_expected.to have_many(:draft_notes) }
it { is_expected.to have_many(:reviews).inverse_of(:merge_request) }
it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
+ it { is_expected.to have_many(:created_environments).class_name('Environment').inverse_of(:merge_request) }
context 'for forks' do
let!(:project) { create(:project) }
@@ -144,22 +145,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
- describe '.attention' do
- let_it_be(:merge_request5) { create(:merge_request, :unique_branches, assignees: [user2]) }
- let_it_be(:merge_request6) { create(:merge_request, :unique_branches, assignees: [user2]) }
-
- before do
- assignee = merge_request6.find_assignee(user2)
- assignee.update!(state: :reviewed)
- merge_request2.find_reviewer(user2).update!(state: :attention_requested)
- merge_request5.find_assignee(user2).update!(state: :attention_requested)
- end
-
- it 'returns MRs that have any attention requests' do
- expect(described_class.attention(user2)).to eq([merge_request2, merge_request5])
- end
- end
-
describe '.drafts' do
it 'returns MRs where draft == true' do
expect(described_class.drafts).to eq([merge_request4])
@@ -884,6 +869,16 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect { subject.cache_merge_request_closes_issues!(subject.author) }
.not_to change(subject.merge_requests_closing_issues, :count)
end
+
+ it 'caches issues from another project with issues enabled' do
+ project = create(:project, :public, issues_enabled: true)
+ issue = create(:issue, project: project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference(full: true)}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }
+ .to change(subject.merge_requests_closing_issues, :count).by(1)
+ end
end
end
@@ -3232,73 +3227,9 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
- describe '#detailed_merge_status' do
- subject(:detailed_merge_status) { merge_request.detailed_merge_status }
-
- context 'when merge status is cannot_be_merged_rechecking' do
- let(:merge_request) { create(:merge_request, merge_status: :cannot_be_merged_rechecking) }
-
- it 'returns :checking' do
- expect(detailed_merge_status).to eq(:checking)
- end
- end
-
- context 'when merge status is preparing' do
- let(:merge_request) { create(:merge_request, merge_status: :preparing) }
-
- it 'returns :checking' do
- expect(detailed_merge_status).to eq(:checking)
- end
- end
-
- context 'when merge status is checking' do
- let(:merge_request) { create(:merge_request, merge_status: :checking) }
-
- it 'returns :checking' do
- expect(detailed_merge_status).to eq(:checking)
- end
- end
-
- context 'when merge status is unchecked' do
- let(:merge_request) { create(:merge_request, merge_status: :unchecked) }
-
- it 'returns :unchecked' do
- expect(detailed_merge_status).to eq(:unchecked)
- end
- end
-
- context 'when merge checks are a success' do
- let(:merge_request) { create(:merge_request) }
-
- it 'returns :mergeable' do
- expect(detailed_merge_status).to eq(:mergeable)
- end
- end
-
- context 'when merge status have a failure' do
- let(:merge_request) { create(:merge_request) }
-
- before do
- merge_request.close!
- end
-
- it 'returns the failure reason' do
- expect(detailed_merge_status).to eq(:not_open)
- end
- end
- end
-
describe '#mergeable_state?' do
it_behaves_like 'for mergeable_state'
- context 'when improved_mergeability_checks is off' do
- before do
- stub_feature_flags(improved_mergeability_checks: false)
- end
-
- it_behaves_like 'for mergeable_state'
- end
-
context 'when merge state caching is off' do
before do
stub_feature_flags(mergeability_caching: false)
@@ -3743,9 +3674,9 @@ RSpec.describe MergeRequest, factory_default: :keep do
let(:expected_diff_refs) do
Gitlab::Diff::DiffRefs.new(
- base_sha: subject.merge_request_diff.base_commit_sha,
+ base_sha: subject.merge_request_diff.base_commit_sha,
start_sha: subject.merge_request_diff.start_commit_sha,
- head_sha: subject.merge_request_diff.head_commit_sha
+ head_sha: subject.merge_request_diff.head_commit_sha
)
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index a48e291fa55..f58d30f81a0 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -9,4 +9,35 @@ RSpec.describe Ml::Candidate do
it { is_expected.to have_many(:params) }
it { is_expected.to have_many(:metrics) }
end
+
+ describe '#new' do
+ it 'iid is not null' do
+ expect(create(:ml_candidates).iid).not_to be_nil
+ end
+ end
+
+ describe 'by_project_id_and_iid' do
+ let_it_be(:candidate) { create(:ml_candidates) }
+
+ let(:project_id) { candidate.experiment.project_id }
+ let(:iid) { candidate.iid }
+
+ subject { described_class.with_project_id_and_iid(project_id, iid) }
+
+ context 'when iid exists', 'and belongs to project' do
+ it { is_expected.to eq(candidate) }
+ end
+
+ context 'when iid exists', 'and does not belong to project' do
+ let(:project_id) { non_existing_record_id }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when iid does not exist' do
+ let(:iid) { 'a' }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index dca5280a8fe..e300f82d290 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -8,4 +8,55 @@ RSpec.describe Ml::Experiment do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
end
+
+ describe '#by_project_id_and_iid?' do
+ let(:exp) { create(:ml_experiments) }
+ let(:iid) { exp.iid }
+
+ subject { described_class.by_project_id_and_iid(exp.project_id, iid) }
+
+ context 'if exists' do
+ it { is_expected.to eq(exp) }
+ end
+
+ context 'if does not exist' do
+ let(:iid) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
+ describe '#by_project_id_and_name?' do
+ let(:exp) { create(:ml_experiments) }
+ let(:exp_name) { exp.name }
+
+ subject { described_class.by_project_id_and_name(exp.project_id, exp_name) }
+
+ context 'if exists' do
+ it { is_expected.to eq(exp) }
+ end
+
+ context 'if does not exist' do
+ let(:exp_name) { 'hello' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#has_record?' do
+ let(:exp) { create(:ml_experiments) }
+ let(:exp_name) { exp.name }
+
+ subject { described_class.has_record?(exp.project_id, exp_name) }
+
+ context 'if exists' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'if does not exist' do
+ let(:exp_name) { 'hello' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 25234db5734..4ac248802b8 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -127,4 +127,54 @@ RSpec.describe NamespaceSetting, type: :model do
end
end
end
+
+ describe '#show_diff_preview_in_email?' do
+ context 'when not a subgroup' do
+ context 'when :show_diff_preview_in_email is false' do
+ it 'returns false' do
+ settings = create(:namespace_settings, show_diff_preview_in_email: false)
+ group = create(:group, namespace_settings: settings )
+
+ expect(group.show_diff_preview_in_email?).to be_falsey
+ end
+ end
+
+ context 'when :show_diff_preview_in_email is true' do
+ it 'returns true' do
+ settings = create(:namespace_settings, show_diff_preview_in_email: true)
+ group = create(:group, namespace_settings: settings )
+
+ expect(group.show_diff_preview_in_email?).to be_truthy
+ end
+ end
+
+ it 'does not query the db when there is no parent group' do
+ group = create(:group)
+
+ expect { group.show_diff_preview_in_email? }.not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'when a group has parent groups' do
+ let(:grandparent) { create(:group, namespace_settings: settings) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let!(:group) { create(:group, parent: parent) }
+
+ context "when a parent group has disabled diff previews" do
+ let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
+
+ it 'returns false' do
+ expect(group.show_diff_preview_in_email?).to be_falsey
+ end
+ end
+
+ context 'when all parent groups have enabled diff previews' do
+ let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
+
+ it 'returns true' do
+ expect(group.show_diff_preview_in_email?).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 71ce3afda44..2e8d22cb9db 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -336,6 +336,26 @@ RSpec.describe Namespace do
expect(described_class.without_project_namespaces).to match_array([namespace, namespace1, namespace2, namespace1sub, namespace2sub, user_namespace, project_namespace.parent])
end
end
+
+ describe '.with_shared_runners_enabled' do
+ subject { described_class.with_shared_runners_enabled }
+
+ context 'when shared runners are enabled for namespace' do
+ let!(:namespace_inheriting_shared_runners) { create(:namespace, shared_runners_enabled: true) }
+
+ it "returns a namespace inheriting shared runners" do
+ is_expected.to include(namespace_inheriting_shared_runners)
+ end
+ end
+
+ context 'when shared runners are disabled for namespace' do
+ let!(:namespace_not_inheriting_shared_runners) { create(:namespace, shared_runners_enabled: false) }
+
+ it "does not return a namespace not inheriting shared runners" do
+ is_expected.not_to include(namespace_not_inheriting_shared_runners)
+ end
+ end
+ end
end
describe 'delegate' do
@@ -439,19 +459,54 @@ RSpec.describe Namespace do
context 'traversal_ids on create' do
shared_examples 'default traversal_ids' do
- let(:namespace) { build(:namespace) }
-
- before do
- namespace.save!
- namespace.reload
- end
+ let!(:namespace) { create(:group) }
+ let!(:child_namespace) { create(:group, parent: namespace) }
- it { expect(namespace.traversal_ids).to eq [namespace.id] }
+ it { expect(namespace.reload.traversal_ids).to eq [namespace.id] }
+ it { expect(child_namespace.reload.traversal_ids).to eq [namespace.id, child_namespace.id] }
+ it { expect(namespace.sync_events.count).to eq 1 }
+ it { expect(child_namespace.sync_events.count).to eq 1 }
end
it_behaves_like 'default traversal_ids'
end
+ context 'traversal_ids on update' do
+ let!(:namespace1) { create(:group) }
+ let!(:namespace2) { create(:group) }
+
+ it 'updates the traversal_ids when the parent_id is changed' do
+ expect do
+ namespace1.update!(parent: namespace2)
+ end.to change { namespace1.reload.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
+ end
+
+ it 'creates a Namespaces::SyncEvent using triggers' do
+ Namespaces::SyncEvent.delete_all
+ namespace1.update!(parent: namespace2)
+ expect(namespace1.reload.sync_events.count).to eq(1)
+ end
+
+ it 'creates sync_events using database trigger on the table' do
+ expect { Group.update_all(traversal_ids: [-1]) }.to change(Namespaces::SyncEvent, :count).by(2)
+ end
+
+ it 'does not create sync_events using database trigger on the table when only the parent_id has changed' do
+ expect { Group.update_all(parent_id: -1) }.not_to change(Namespaces::SyncEvent, :count)
+ end
+
+ it 'triggers the callback sync_traversal_ids on the namespace' do
+ allow(namespace1).to receive(:run_callbacks).and_call_original
+ expect(namespace1).to receive(:run_callbacks).with(:sync_traversal_ids)
+ namespace1.update!(parent: namespace2)
+ end
+
+ it 'calls schedule_sync_event_worker on the updated namespace' do
+ expect(namespace1).to receive(:schedule_sync_event_worker)
+ namespace1.update!(parent: namespace2)
+ end
+ end
+
describe "after_commit :expire_child_caches" do
let(:namespace) { create(:group) }
@@ -675,6 +730,24 @@ RSpec.describe Namespace do
end
end
+ describe '#any_project_with_shared_runners_enabled?' do
+ subject { namespace.any_project_with_shared_runners_enabled? }
+
+ let!(:project_not_inheriting_shared_runners) do
+ create(:project, namespace: namespace, shared_runners_enabled: false)
+ end
+
+ context 'when a child project has shared runners enabled' do
+ let!(:project_inheriting_shared_runners) { create(:project, namespace: namespace, shared_runners_enabled: true) }
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when all child projects have shared runners disabled' do
+ it { is_expected.to eq false }
+ end
+ end
+
describe '.search' do
let_it_be(:first_group) { create(:group, name: 'my first namespace', path: 'old-path') }
let_it_be(:parent_group) { create(:group, name: 'my parent namespace', path: 'parent-path') }
@@ -744,30 +817,30 @@ RSpec.describe Namespace do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 101,
- wiki_size: 505,
- lfs_objects_size: 202,
- build_artifacts_size: 303,
+ namespace: namespace,
+ repository_size: 101,
+ wiki_size: 505,
+ lfs_objects_size: 202,
+ build_artifacts_size: 303,
pipeline_artifacts_size: 707,
- packages_size: 404,
- snippets_size: 605,
- uploads_size: 808))
+ packages_size: 404,
+ snippets_size: 605,
+ uploads_size: 808))
end
let(:project2) do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 10,
- wiki_size: 50,
- lfs_objects_size: 20,
- build_artifacts_size: 30,
+ namespace: namespace,
+ repository_size: 10,
+ wiki_size: 50,
+ lfs_objects_size: 20,
+ build_artifacts_size: 30,
pipeline_artifacts_size: 70,
- packages_size: 40,
- snippets_size: 60,
- uploads_size: 80))
+ packages_size: 40,
+ snippets_size: 60,
+ uploads_size: 80))
end
it "sums all project storage counters in the namespace" do
@@ -2216,6 +2289,20 @@ RSpec.describe Namespace do
expect(namespace.sync_events.count).to eq(2)
end
+ it 'creates a namespaces_sync_event for the parent and all the descendent namespaces' do
+ children_namespaces = create_list(:group, 2, parent_id: namespace.id)
+ grand_children_namespaces = create_list(:group, 2, parent_id: children_namespaces.first.id)
+ expect(Namespaces::ProcessSyncEventsWorker).to receive(:perform_async).exactly(:once)
+ Namespaces::SyncEvent.delete_all
+
+ expect do
+ namespace.update!(parent_id: new_namespace1.id)
+ end.to change(Namespaces::SyncEvent, :count).by(5)
+
+ expected_ids = [namespace.id] + children_namespaces.map(&:id) + grand_children_namespaces.map(&:id)
+ expect(Namespaces::SyncEvent.pluck(:namespace_id)).to match_array(expected_ids)
+ end
+
it 'enqueues ProcessSyncEventsWorker' do
expect(Namespaces::ProcessSyncEventsWorker).to receive(:perform_async)
diff --git a/spec/models/namespaces/sync_event_spec.rb b/spec/models/namespaces/sync_event_spec.rb
new file mode 100644
index 00000000000..a3a90ba9aff
--- /dev/null
+++ b/spec/models/namespaces/sync_event_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::SyncEvent, type: :model do
+ describe '.enqueue_worker' do
+ it 'schedules Namespaces::ProcessSyncEventsWorker job' do
+ expect(::Namespaces::ProcessSyncEventsWorker).to receive(:perform_async)
+ described_class.enqueue_worker
+ end
+ end
+
+ describe '.upper_bound_count' do
+ it 'returns 0 when there are no records in the table' do
+ expect(described_class.upper_bound_count).to eq(0)
+ end
+
+ it 'returns an estimated number of the records in the database' do
+ create_list(:namespace, 3)
+ expect(described_class.upper_bound_count).to eq(3)
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index ca558848cb0..1fce1f97dcb 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -280,6 +280,32 @@ RSpec.describe Note do
expect { note.destroy! }.not_to raise_error
end
end
+
+ describe 'sets internal flag' do
+ subject(:internal) { note.reload.internal }
+
+ let(:note) { create(:note, confidential: confidential, project: issue.project, noteable: issue) }
+
+ let_it_be(:issue) { create(:issue) }
+
+ context 'when confidential is `true`' do
+ let(:confidential) { true }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when confidential is `false`' do
+ let(:confidential) { false }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when confidential is `nil`' do
+ let(:confidential) { nil }
+
+ it { is_expected.to be false }
+ end
+ end
end
describe "Commit notes" do
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 4debda0621c..8105262aada 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -39,6 +39,56 @@ RSpec.describe NotificationRecipient do
expect(recipient.notifiable?).to eq true
end
end
+
+ context 'when recipient email is blocked', :clean_gitlab_redis_rate_limiting do
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
+ .and_return(
+ temporary_email_failure: { threshold: 1, interval: 1.minute },
+ permanent_email_failure: { threshold: 1, interval: 1.minute }
+ )
+ end
+
+ context 'with permanent failures' do
+ before do
+ 2.times { Gitlab::ApplicationRateLimiter.throttled?(:permanent_email_failure, scope: user.email) }
+ end
+
+ it 'returns false' do
+ expect(recipient.notifiable?).to eq(false)
+ end
+
+ context 'when block_emails_with_failures is disabled' do
+ before do
+ stub_feature_flags(block_emails_with_failures: false)
+ end
+
+ it 'returns true' do
+ expect(recipient.notifiable?).to eq(true)
+ end
+ end
+ end
+
+ context 'with temporary failures' do
+ before do
+ 2.times { Gitlab::ApplicationRateLimiter.throttled?(:temporary_email_failure, scope: user.email) }
+ end
+
+ it 'returns false' do
+ expect(recipient.notifiable?).to eq(false)
+ end
+
+ context 'when block_emails_with_failures is disabled' do
+ before do
+ stub_feature_flags(block_emails_with_failures: false)
+ end
+
+ it 'returns true' do
+ expect(recipient.notifiable?).to eq(true)
+ end
+ end
+ end
+ end
end
describe '#has_access?' do
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
index 544f6643712..a4540ac95bc 100644
--- a/spec/models/oauth_access_token_spec.rb
+++ b/spec/models/oauth_access_token_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe OauthAccessToken do
- let(:user) { create(:user) }
let(:app_one) { create(:oauth_application) }
let(:app_two) { create(:oauth_application) }
let(:app_three) { create(:oauth_application) }
@@ -69,4 +68,20 @@ RSpec.describe OauthAccessToken do
end
end
end
+
+ describe '.matching_token_for' do
+ it 'does not find existing tokens' do
+ expect(described_class.matching_token_for(app_one, token.resource_owner, token.scopes)).to be_nil
+ end
+
+ context 'when hash oauth tokens is disabled' do
+ before do
+ stub_feature_flags(hash_oauth_tokens: false)
+ end
+
+ it 'finds an existing token' do
+ expect(described_class.matching_token_for(app_one, token.resource_owner, token.scopes)).to be_present
+ end
+ end
+ end
end
diff --git a/spec/lib/learn_gitlab/onboarding_spec.rb b/spec/models/onboarding/completion_spec.rb
index 3e22ce59091..e1fad4255bc 100644
--- a/spec/lib/learn_gitlab/onboarding_spec.rb
+++ b/spec/models/onboarding/completion_spec.rb
@@ -2,28 +2,24 @@
require 'spec_helper'
-RSpec.describe LearnGitlab::Onboarding do
- describe '#completed_percentage' do
+RSpec.describe Onboarding::Completion do
+ describe '#percentage' do
let(:completed_actions) { {} }
- let(:onboarding_progress) { build(:onboarding_progress, namespace: namespace, **completed_actions) }
- let(:namespace) { create(:namespace) }
-
- let_it_be(:tracked_action_columns) do
+ let!(:onboarding_progress) { create(:onboarding_progress, namespace: namespace, **completed_actions) }
+ let(:tracked_action_columns) do
[
*described_class::ACTION_ISSUE_IDS.keys,
*described_class::ACTION_PATHS,
:security_scan_enabled
- ].map { |key| OnboardingProgress.column_name(key) }
+ ].map { |key| ::Onboarding::Progress.column_name(key) }
end
- before do
- expect(OnboardingProgress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress)
- end
+ let_it_be(:namespace) { create(:namespace) }
- subject { described_class.new(namespace).completed_percentage }
+ subject { described_class.new(namespace).percentage }
context 'when no onboarding_progress exists' do
- let(:onboarding_progress) { nil }
+ subject { described_class.new(build(:namespace)).percentage }
it { is_expected.to eq(0) }
end
@@ -34,13 +30,13 @@ RSpec.describe LearnGitlab::Onboarding do
context 'when all tracked actions have been completed' do
let(:completed_actions) do
- tracked_action_columns.to_h { |action| [action, Time.current] }
+ tracked_action_columns.index_with { Time.current }
end
it { is_expected.to eq(100) }
end
- describe 'security_actions_continuous_onboarding experiment' do
+ context 'with security_actions_continuous_onboarding experiment' do
let(:completed_actions) { Hash[tracked_action_columns.first, Time.current] }
context 'when control' do
diff --git a/spec/lib/learn_gitlab/project_spec.rb b/spec/models/onboarding/learn_gitlab_spec.rb
index 23784709817..5e3e1f9c304 100644
--- a/spec/lib/learn_gitlab/project_spec.rb
+++ b/spec/models/onboarding/learn_gitlab_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe LearnGitlab::Project do
+RSpec.describe Onboarding::LearnGitlab do
let_it_be(:current_user) { create(:user) }
- let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) }
- let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) }
- let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) }
+ let_it_be(:learn_gitlab_project) { create(:project, name: described_class::PROJECT_NAME) }
+ let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: described_class::BOARD_NAME) }
+ let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: described_class::LABEL_NAME) }
before do
learn_gitlab_project.add_developer(current_user)
end
- describe '.available?' do
+ describe '#available?' do
using RSpec::Parameterized::TableSyntax
where(:project, :board, :label, :expected_result) do
@@ -41,25 +41,27 @@ RSpec.describe LearnGitlab::Project do
end
end
- describe '.project' do
+ describe '#project' do
subject { described_class.new(current_user).project }
it { is_expected.to eq learn_gitlab_project }
context 'when it is created during trial signup' do
- let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL, path: 'learn-gitlab-ultimate-trial') }
+ let_it_be(:learn_gitlab_project) do
+ create(:project, name: described_class::PROJECT_NAME_ULTIMATE_TRIAL, path: 'learn-gitlab-ultimate-trial')
+ end
it { is_expected.to eq learn_gitlab_project }
end
end
- describe '.board' do
+ describe '#board' do
subject { described_class.new(current_user).board }
it { is_expected.to eq learn_gitlab_board }
end
- describe '.label' do
+ describe '#label' do
subject { described_class.new(current_user).label }
it { is_expected.to eq learn_gitlab_label }
diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding/progress_spec.rb
index 9688dd01c71..9d91af2487a 100644
--- a/spec/models/onboarding_progress_spec.rb
+++ b/spec/models/onboarding/progress_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe OnboardingProgress do
+RSpec.describe Onboarding::Progress do
let(:namespace) { create(:namespace) }
let(:action) { :subscription_created }
@@ -34,7 +34,9 @@ RSpec.describe OnboardingProgress do
subject { described_class.incomplete_actions(actions) }
let!(:no_actions_completed) { create(:onboarding_progress) }
- let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
+ let!(:one_action_completed_one_action_incompleted) do
+ create(:onboarding_progress, "#{action}_at" => Time.current)
+ end
context 'when given one action' do
let(:actions) { action }
@@ -52,8 +54,13 @@ RSpec.describe OnboardingProgress do
describe '.completed_actions' do
subject { described_class.completed_actions(actions) }
- let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
- let!(:both_actions_completed) { create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) }
+ let!(:one_action_completed_one_action_incompleted) do
+ create(:onboarding_progress, "#{action}_at" => Time.current)
+ end
+
+ let!(:both_actions_completed) do
+ create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current)
+ end
context 'when given one action' do
let(:actions) { action }
@@ -69,12 +76,23 @@ RSpec.describe OnboardingProgress do
end
describe '.completed_actions_with_latest_in_range' do
- subject { described_class.completed_actions_with_latest_in_range(actions, 1.day.ago.beginning_of_day..1.day.ago.end_of_day) }
+ subject do
+ described_class.completed_actions_with_latest_in_range(actions,
+ 1.day.ago.beginning_of_day..1.day.ago.end_of_day)
+ end
+
+ let!(:one_action_completed_in_range_one_action_incompleted) do
+ create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day)
+ end
- let!(:one_action_completed_in_range_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) }
let!(:git_write_action_completed_in_range) { create(:onboarding_progress, git_write_at: 1.day.ago.middle_of_day) }
- let!(:both_actions_completed_latest_action_out_of_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) }
- let!(:both_actions_completed_latest_action_in_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) }
+ let!(:both_actions_completed_latest_action_out_of_range) do
+ create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current)
+ end
+
+ let!(:both_actions_completed_latest_action_in_range) do
+ create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day)
+ end
context 'when given one action' do
let(:actions) { :git_write }
@@ -147,7 +165,9 @@ RSpec.describe OnboardingProgress do
expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).to be_nil
register_action
expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).not_to be_nil
- expect { described_class.register(namespace, action) }.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at }
+ expect do
+ described_class.register(namespace, action)
+ end.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at }
end
context 'when the action does not exist' do
@@ -209,7 +229,9 @@ RSpec.describe OnboardingProgress do
context 'when the namespace was not onboarded' do
it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action1) }.from(false)
- expect { described_class.register(namespace, action) }.not_to change { described_class.completed?(namespace, action2) }.from(false)
+ expect do
+ described_class.register(namespace, action)
+ end.not_to change { described_class.completed?(namespace, action2) }.from(false)
end
end
end
@@ -270,21 +292,23 @@ RSpec.describe OnboardingProgress do
end
describe '#number_of_completed_actions' do
- subject { build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions }
+ subject do
+ build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions
+ end
- context '0 completed actions' do
+ context 'with 0 completed actions' do
let(:actions) { [:created_at, :updated_at] }
it { is_expected.to eq(0) }
end
- context '1 completed action' do
+ context 'with 1 completed action' do
let(:actions) { [:created_at, :subscription_created_at] }
it { is_expected.to eq(1) }
end
- context '2 completed actions' do
+ context 'with 2 completed actions' do
let(:actions) { [:subscription_created_at, :git_write_at] }
it { is_expected.to eq(2) }
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
index e709470b312..85a475f5c53 100644
--- a/spec/models/operations/feature_flag_spec.rb
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Operations::FeatureFlag do
it 'is valid if associated with Operations::FeatureFlags::Strategy models' do
project = create(:project)
feature_flag = described_class.create!({ name: 'test', project: project, version: 2,
- strategies_attributes: [{ name: 'default', parameters: {} }] })
+ strategies_attributes: [{ name: 'default', parameters: {} }] })
expect(feature_flag).to be_valid
end
@@ -114,13 +114,11 @@ RSpec.describe Operations::FeatureFlag do
let_it_be(:project) { create(:project) }
let!(:feature_flag) do
- create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
+ create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
end
let!(:strategy) do
- create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
end
it 'matches wild cards in the scope' do
@@ -141,10 +139,8 @@ RSpec.describe Operations::FeatureFlag do
it 'returns feature flags ordered by id' do
create(:operations_scope, strategy: strategy, environment_scope: 'production')
- feature_flag_b = create(:operations_feature_flag, project: project,
- name: 'feature2', active: true, version: 2)
- strategy_b = create(:operations_strategy, feature_flag: feature_flag_b,
- name: 'default', parameters: {})
+ feature_flag_b = create(:operations_feature_flag, project: project, name: 'feature2', active: true, version: 2)
+ strategy_b = create(:operations_strategy, feature_flag: feature_flag_b, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy_b, environment_scope: '*')
flags = described_class.for_unleash_client(project, 'production')
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 526c57d08b0..fb88dbb4212 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
+ it { is_expected.to have_one(:rpm_metadatum).inverse_of(:package) }
end
describe '.with_debian_codename' do
@@ -1356,4 +1357,16 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to eq(normalized_name) }
end
end
+
+ describe '#touch_last_downloaded_at' do
+ let_it_be(:package) { create(:package) }
+
+ subject { package.touch_last_downloaded_at }
+
+ it 'updates the downloaded_at' do
+ expect(::Gitlab::Database::LoadBalancing::Session).to receive(:without_sticky_writes).and_call_original
+ expect { subject }
+ .to change(package, :last_downloaded_at).from(nil).to(instance_of(ActiveSupport::TimeWithZone))
+ end
+ end
end
diff --git a/spec/models/packages/rpm/metadatum_spec.rb b/spec/models/packages/rpm/metadatum_spec.rb
new file mode 100644
index 00000000000..0e7817fdf86
--- /dev/null
+++ b/spec/models/packages/rpm/metadatum_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::Metadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:package) }
+ it { is_expected.to validate_presence_of(:epoch) }
+ it { is_expected.to validate_presence_of(:release) }
+ it { is_expected.to validate_presence_of(:summary) }
+ it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_presence_of(:arch) }
+
+ it { is_expected.to validate_numericality_of(:epoch).only_integer.is_greater_than_or_equal_to(0) }
+
+ it { is_expected.to validate_length_of(:release).is_at_most(128) }
+ it { is_expected.to validate_length_of(:summary).is_at_most(1000) }
+ it { is_expected.to validate_length_of(:description).is_at_most(5000) }
+ it { is_expected.to validate_length_of(:arch).is_at_most(255) }
+ it { is_expected.to validate_length_of(:license).is_at_most(1000) }
+ it { is_expected.to validate_length_of(:url).is_at_most(1000) }
+
+ describe '#rpm_package_type' do
+ it 'will not allow a package with a different package_type' do
+ package = build('conan_package')
+ rpm_metadatum = build('rpm_metadatum', package: package)
+
+ expect(rpm_metadatum).not_to be_valid
+ expect(rpm_metadatum.errors.to_a).to include('Package type must be RPM')
+ end
+ end
+ end
+end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 4e463b1194c..b50bfaed528 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -21,6 +21,15 @@ RSpec.describe PagesDomain do
end
end
+ describe '.verified' do
+ let!(:verified) { create(:pages_domain) }
+ let!(:unverified) { create(:pages_domain, :unverified) }
+
+ it 'finds verified' do
+ expect(described_class.verified).to match_array(verified)
+ end
+ end
+
describe 'validate domain' do
subject(:pages_domain) { build(:pages_domain, domain: domain) }
@@ -32,17 +41,17 @@ RSpec.describe PagesDomain do
describe "hostname" do
{
- 'my.domain.com' => true,
- '123.456.789' => true,
- '0x12345.com' => true,
- '0123123' => true,
- 'a-reserved.com' => true,
+ 'my.domain.com' => true,
+ '123.456.789' => true,
+ '0x12345.com' => true,
+ '0123123' => true,
+ 'a-reserved.com' => true,
'a.b-reserved.com' => true,
- 'reserved.com' => true,
- '_foo.com' => false,
- 'a.reserved.com' => false,
+ 'reserved.com' => true,
+ '_foo.com' => false,
+ 'a.reserved.com' => false,
'a.b.reserved.com' => false,
- nil => false
+ nil => false
}.each do |value, validity|
context "domain #{value.inspect} validity" do
before do
@@ -62,12 +71,11 @@ RSpec.describe PagesDomain do
let(:domain) { 'my.domain.com' }
let(:project) do
- instance_double(Project, pages_https_only?: pages_https_only)
+ instance_double(Project, pages_https_only?: pages_https_only, can_create_custom_domains?: true)
end
let(:pages_domain) do
- build(:pages_domain, certificate: certificate, key: key,
- auto_ssl_enabled: auto_ssl_enabled).tap do |pd|
+ build(:pages_domain, certificate: certificate, key: key, auto_ssl_enabled: auto_ssl_enabled).tap do |pd|
allow(pd).to receive(:project).and_return(project)
pd.valid?
end
@@ -572,6 +580,32 @@ RSpec.describe PagesDomain do
end
end
+ describe '#validate_custom_domain_count_per_project' do
+ let_it_be(:project) { create(:project) }
+
+ context 'when max custom domain setting is set to 0' do
+ it 'returns without an error' do
+ pages_domain = create(:pages_domain, project: project)
+
+ expect(pages_domain).to be_valid
+ end
+ end
+
+ context 'when max custom domain setting is not set to 0' do
+ it 'returns with an error for extra domains' do
+ Gitlab::CurrentSettings.update!(max_pages_custom_domains_per_project: 1)
+
+ pages_domain = create(:pages_domain, project: project)
+ expect(pages_domain).to be_valid
+
+ pages_domain = build(:pages_domain, project: project)
+ expect(pages_domain).not_to be_valid
+ expect(pages_domain.errors.full_messages)
+ .to contain_exactly('This project reached the limit of custom domains. (Max 1)')
+ end
+ end
+ end
+
describe '.find_by_domain_case_insensitive' do
it 'lookup is case-insensitive' do
pages_domain = create(:pages_domain, domain: "Pages.IO")
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index f3ef347121e..5bce6a2cc3f 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -64,6 +64,81 @@ RSpec.describe PersonalAccessToken do
expect(described_class.for_users([user_1, user_2])).to contain_exactly(token_of_user_1, token_of_user_2)
end
end
+
+ describe '.created_before' do
+ let(:last_used_at) { 1.month.ago.beginning_of_hour }
+ let!(:new_used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at + 1.minute,
+ last_used_at: last_used_at + 1.minute
+ )
+ end
+
+ let!(:old_unused_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute
+ )
+ end
+
+ let!(:old_formerly_used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute,
+ last_used_at: last_used_at - 1.minute
+ )
+ end
+
+ let!(:old_still_used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute,
+ last_used_at: 1.minute.ago
+ )
+ end
+
+ subject { described_class.created_before(last_used_at) }
+
+ it do
+ is_expected.to contain_exactly(
+ old_unused_token,
+ old_formerly_used_token,
+ old_still_used_token
+ )
+ end
+ end
+
+ describe '.last_used_before_or_unused' do
+ let(:last_used_at) { 1.month.ago.beginning_of_hour }
+ let!(:unused_token) { create(:personal_access_token) }
+ let!(:used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at + 1.minute,
+ last_used_at: last_used_at + 1.minute
+ )
+ end
+
+ let!(:old_unused_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute
+ )
+ end
+
+ let!(:old_formerly_used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute,
+ last_used_at: last_used_at - 1.minute
+ )
+ end
+
+ let!(:old_still_used_token) do
+ create(:personal_access_token,
+ created_at: last_used_at - 1.minute,
+ last_used_at: 1.minute.ago
+ )
+ end
+
+ subject { described_class.last_used_before_or_unused(last_used_at) }
+
+ it { is_expected.to contain_exactly(old_unused_token, old_formerly_used_token) }
+ end
end
describe ".active?" do
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index 447b7b2e0a2..bf88e941540 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -43,6 +43,15 @@ RSpec.describe PoolRepository do
end
end
+ context 'when skipping disconnect' do
+ it 'does not change the alternates file' do
+ before = File.read(alternates_file)
+ pool.unlink_repository(pool.source_project.repository, disconnect: false)
+
+ expect(File.read(alternates_file)).to eq(before)
+ end
+ end
+
context 'when the second member leaves' do
it 'does not schedule pool removal' do
other_project = create(:project, :repository, pool_repository: pool)
diff --git a/spec/models/preloaders/project_policy_preloader_spec.rb b/spec/models/preloaders/project_policy_preloader_spec.rb
new file mode 100644
index 00000000000..79f232f5ce2
--- /dev/null
+++ b/spec/models/preloaders/project_policy_preloader_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::ProjectPolicyPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:root_parent) { create(:group, :private, name: 'root-1', path: 'root-1') }
+ let_it_be(:guest_project) { create(:project, name: 'public guest', path: 'public-guest') }
+ let_it_be(:private_maintainer_project) do
+ create(:project, :private, name: 'b private maintainer', path: 'b-private-maintainer', namespace: root_parent)
+ end
+
+ let_it_be(:private_developer_project) do
+ create(:project, :private, name: 'c public developer', path: 'c-public-developer')
+ end
+
+ let_it_be(:public_maintainer_project) do
+ create(:project, :private, name: 'a public maintainer', path: 'a-public-maintainer')
+ end
+
+ let(:base_projects) do
+ Project.where(id: [guest_project, private_maintainer_project, private_developer_project, public_maintainer_project])
+ end
+
+ before_all do
+ guest_project.add_guest(user)
+ private_maintainer_project.add_maintainer(user)
+ private_developer_project.add_developer(user)
+ public_maintainer_project.add_maintainer(user)
+ end
+
+ it 'avoids N+1 queries when authorizing a list of projects', :request_store do
+ preload_projects_for_policy(user)
+ control = ActiveRecord::QueryRecorder.new { authorize_all_projects(user) }
+
+ new_project1 = create(:project, :private).tap { |project| project.add_maintainer(user) }
+ new_project2 = create(:project, :private, namespace: root_parent)
+
+ another_root = create(:group, :private, name: 'root-3', path: 'root-3')
+ new_project3 = create(:project, :private, namespace: another_root).tap { |project| project.add_maintainer(user) }
+
+ pristine_projects = Project.where(id: base_projects + [new_project1, new_project2, new_project3])
+
+ preload_projects_for_policy(user, pristine_projects)
+ expect { authorize_all_projects(user, pristine_projects) }.not_to exceed_query_limit(control)
+ end
+
+ def authorize_all_projects(current_user, project_list = base_projects)
+ project_list.each { |project| current_user.can?(:read_project, project) }
+ end
+
+ def preload_projects_for_policy(current_user, project_list = base_projects)
+ described_class.new(project_list, current_user).execute
+ end
+end
diff --git a/spec/models/preloaders/project_root_ancestor_preloader_spec.rb b/spec/models/preloaders/project_root_ancestor_preloader_spec.rb
new file mode 100644
index 00000000000..30036a6a033
--- /dev/null
+++ b/spec/models/preloaders/project_root_ancestor_preloader_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::ProjectRootAncestorPreloader do
+ let_it_be(:root_parent1) { create(:group, :private, name: 'root-1', path: 'root-1') }
+ let_it_be(:root_parent2) { create(:group, :private, name: 'root-2', path: 'root-2') }
+ let_it_be(:guest_project) { create(:project, name: 'public guest', path: 'public-guest') }
+ let_it_be(:private_maintainer_project) do
+ create(:project, :private, name: 'b private maintainer', path: 'b-private-maintainer', namespace: root_parent1)
+ end
+
+ let_it_be(:private_developer_project) do
+ create(:project, :private, name: 'c public developer', path: 'c-public-developer')
+ end
+
+ let_it_be(:public_maintainer_project) do
+ create(:project, :private, name: 'a public maintainer', path: 'a-public-maintainer', namespace: root_parent2)
+ end
+
+ let(:root_query_regex) { /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/ }
+ let(:additional_preloads) { [] }
+ let(:projects) { [guest_project, private_maintainer_project, private_developer_project, public_maintainer_project] }
+ let(:pristine_projects) { Project.where(id: projects) }
+
+ shared_examples 'executes N matching DB queries' do |expected_query_count, query_method = nil|
+ it 'executes the specified root_ancestor queries' do
+ expect do
+ pristine_projects.each do |project|
+ root_ancestor = project.root_ancestor
+
+ root_ancestor.public_send(query_method) if query_method.present?
+ end
+ end.to make_queries_matching(root_query_regex, expected_query_count)
+ end
+
+ it 'strong_memoizes the correct root_ancestor' do
+ pristine_projects.each do |project|
+ expected_parent_id = project.root_ancestor&.id
+
+ expect(project.parent_id).to eq(expected_parent_id)
+ end
+ end
+ end
+
+ context 'when use_traversal_ids FF is enabled' do
+ context 'when the preloader is used' do
+ before do
+ preload_ancestors
+ end
+
+ context 'when no additional preloads are provided' do
+ it_behaves_like 'executes N matching DB queries', 0
+ end
+
+ context 'when additional preloads are provided' do
+ let(:additional_preloads) { [:route] }
+ let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ }
+
+ it_behaves_like 'executes N matching DB queries', 0, :full_path
+ end
+ end
+
+ context 'when the preloader is not used' do
+ it_behaves_like 'executes N matching DB queries', 4
+ end
+ end
+
+ context 'when use_traversal_ids FF is disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
+
+ context 'when the preloader is used' do
+ before do
+ preload_ancestors
+ end
+
+ context 'when no additional preloads are provided' do
+ it_behaves_like 'executes N matching DB queries', 4
+ end
+
+ context 'when additional preloads are provided' do
+ let(:additional_preloads) { [:route] }
+ let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ }
+
+ it_behaves_like 'executes N matching DB queries', 4, :full_path
+ end
+ end
+
+ context 'when the preloader is not used' do
+ it_behaves_like 'executes N matching DB queries', 4
+ end
+ end
+
+ def preload_ancestors
+ described_class.new(pristine_projects, :namespace, additional_preloads).execute
+ end
+end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index fb1601a5f9c..a09ae7ec7ae 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -63,4 +63,51 @@ RSpec.describe ProjectSetting, type: :model do
target_platforms.permutation(n).to_a
end
end
+
+ describe '#show_diff_preview_in_email?' do
+ context 'when a project is a top-level namespace' do
+ let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: false) }
+ let(:project) { create(:project, project_setting: project_settings) }
+
+ context 'when show_diff_preview_in_email is disabled' do
+ it 'returns false' do
+ expect(project).not_to be_show_diff_preview_in_email
+ end
+ end
+
+ context 'when show_diff_preview_in_email is enabled' do
+ let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: true) }
+
+ it 'returns true' do
+ settings = create(:project_setting, show_diff_preview_in_email: true)
+ project = create(:project, project_setting: settings)
+
+ expect(project).to be_show_diff_preview_in_email
+ end
+ end
+ end
+
+ context 'when a parent group has a parent group' do
+ let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
+ let(:project_settings) { create(:project_setting, show_diff_preview_in_email: true) }
+ let(:group) { create(:group, namespace_settings: namespace_settings) }
+ let!(:project) { create(:project, namespace_id: group.id, project_setting: project_settings) }
+
+ context 'when show_diff_preview_in_email is disabled for the parent group' do
+ it 'returns false' do
+ expect(project).not_to be_show_diff_preview_in_email
+ end
+ end
+
+ context 'when all ancestors have enabled diff previews' do
+ let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
+
+ it 'returns true' do
+ group.update_attribute(:show_diff_preview_in_email, true)
+
+ expect(project).to be_show_diff_preview_in_email
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 98b202299a8..99b984ff547 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -366,12 +366,35 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to include_module(Sortable) }
end
+ describe 'before_validation' do
+ context 'with removal of leading spaces' do
+ subject(:project) { build(:project, name: ' space first', path: 'some_path') }
+
+ it 'removes the leading space' do
+ expect(project.name).to eq ' space first'
+
+ expect(project).to be_valid # triggers before_validation and assures we automatically handle the bad format
+
+ expect(project.name).to eq 'space first'
+ end
+
+ context 'when name is nil' do
+ it 'falls through to the presence validation' do
+ project.name = nil
+
+ expect(project).not_to be_valid
+ end
+ end
+ end
+ end
+
describe 'validation' do
let!(:project) { create(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ it { is_expected.to allow_value('space last ').for(:name) }
it { is_expected.not_to allow_value('colon:in:path').for(:path) } # This is to validate that a specially crafted name cannot bypass a pattern match. See !72555
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_length_of(:path).is_at_most(255) }
@@ -1736,8 +1759,8 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '.with_shared_runners' do
- subject { described_class.with_shared_runners }
+ describe '.with_shared_runners_enabled' do
+ subject { described_class.with_shared_runners_enabled }
context 'when shared runners are enabled for project' do
let!(:project) { create(:project, shared_runners_enabled: true) }
@@ -3925,162 +3948,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#ci_variables_for' do
- let_it_be(:project) { create(:project) }
-
- let(:environment_scope) { '*' }
-
- let!(:ci_variable) do
- create(:ci_variable, value: 'secret', project: project, environment_scope: environment_scope)
- end
-
- let!(:protected_variable) do
- create(:ci_variable, :protected, value: 'protected', project: project)
- end
-
- subject { project.reload.ci_variables_for(ref: 'ref') }
-
- before do
- stub_application_setting(
- default_branch_protection: Gitlab::Access::PROTECTION_NONE)
- end
-
- shared_examples 'ref is protected' do
- it 'contains all the variables' do
- is_expected.to contain_exactly(ci_variable, protected_variable)
- end
- end
-
- it 'memoizes the result by ref and environment', :request_store do
- scoped_variable = create(:ci_variable, value: 'secret', project: project, environment_scope: 'scoped')
-
- expect(project).to receive(:protected_for?).with('ref').once.and_return(true)
- expect(project).to receive(:protected_for?).with('other').twice.and_return(false)
-
- 2.times do
- expect(project.reload.ci_variables_for(ref: 'ref', environment: 'production')).to contain_exactly(ci_variable, protected_variable)
- expect(project.reload.ci_variables_for(ref: 'other')).to contain_exactly(ci_variable)
- expect(project.reload.ci_variables_for(ref: 'other', environment: 'scoped')).to contain_exactly(ci_variable, scoped_variable)
- end
- end
-
- context 'when the ref is not protected' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(false)
- end
-
- it 'contains only the CI variables' do
- is_expected.to contain_exactly(ci_variable)
- end
- end
-
- context 'when the ref is a protected branch' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(true)
- end
-
- it_behaves_like 'ref is protected'
- end
-
- context 'when the ref is a protected tag' do
- before do
- allow(project).to receive(:protected_for?).with('ref').and_return(true)
- end
-
- it_behaves_like 'ref is protected'
- end
-
- context 'when environment name is specified' do
- let(:environment) { 'review/name' }
-
- subject do
- project.ci_variables_for(ref: 'ref', environment: environment)
- end
-
- context 'when environment scope is exactly matched' do
- let(:environment_scope) { 'review/name' }
-
- it { is_expected.to contain_exactly(ci_variable) }
- end
-
- context 'when environment scope is matched by wildcard' do
- let(:environment_scope) { 'review/*' }
-
- it { is_expected.to contain_exactly(ci_variable) }
- end
-
- context 'when environment scope does not match' do
- let(:environment_scope) { 'review/*/special' }
-
- it { is_expected.not_to contain_exactly(ci_variable) }
- end
-
- context 'when environment scope has _' do
- let(:environment_scope) { '*_*' }
-
- it 'does not treat it as wildcard' do
- is_expected.not_to contain_exactly(ci_variable)
- end
-
- context 'when environment name contains underscore' do
- let(:environment) { 'foo_bar/test' }
- let(:environment_scope) { 'foo_bar/*' }
-
- it 'matches literally for _' do
- is_expected.to contain_exactly(ci_variable)
- end
- end
- end
-
- # The environment name and scope cannot have % at the moment,
- # but we're considering relaxing it and we should also make sure
- # it doesn't break in case some data sneaked in somehow as we're
- # not checking this integrity in database level.
- context 'when environment scope has %' do
- it 'does not treat it as wildcard' do
- ci_variable.update_attribute(:environment_scope, '*%*')
-
- is_expected.not_to contain_exactly(ci_variable)
- end
-
- context 'when environment name contains a percent' do
- let(:environment) { 'foo%bar/test' }
-
- it 'matches literally for _' do
- ci_variable.environment_scope = 'foo%bar/*'
-
- is_expected.to contain_exactly(ci_variable)
- end
- end
- end
-
- context 'when variables with the same name have different environment scopes' do
- let!(:partially_matched_variable) do
- create(:ci_variable,
- key: ci_variable.key,
- value: 'partial',
- environment_scope: 'review/*',
- project: project)
- end
-
- let!(:perfectly_matched_variable) do
- create(:ci_variable,
- key: ci_variable.key,
- value: 'prefect',
- environment_scope: 'review/name',
- project: project)
- end
-
- it 'puts variables matching environment scope more in the end' do
- is_expected.to eq(
- [ci_variable,
- partially_matched_variable,
- perfectly_matched_variable])
- end
- end
- end
- end
-
describe '#any_lfs_file_locks?', :request_store do
let_it_be(:project) { create(:project) }
@@ -6290,14 +6157,6 @@ RSpec.describe Project, factory_default: :keep do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :group, groups: [group]) }
it { is_expected.to eq(deploy_token) }
-
- context 'when the FF ci_variable_for_group_gitlab_deploy_token is disabled' do
- before do
- stub_feature_flags(ci_variable_for_group_gitlab_deploy_token: false)
- end
-
- it { is_expected.to be_nil }
- end
end
context 'when the project and its group has a gitlab deploy token associated' do
@@ -6307,14 +6166,6 @@ RSpec.describe Project, factory_default: :keep do
let!(:group_deploy_token) { create(:deploy_token, :gitlab_deploy_token, :group, groups: [group]) }
it { is_expected.to eq(project_deploy_token) }
-
- context 'when the FF ci_variable_for_group_gitlab_deploy_token is disabled' do
- before do
- stub_feature_flags(ci_variable_for_group_gitlab_deploy_token: false)
- end
-
- it { is_expected.to eq(project_deploy_token) }
- end
end
end
@@ -6624,11 +6475,27 @@ RSpec.describe Project, factory_default: :keep do
let(:pool) { create(:pool_repository) }
let(:project) { create(:project, :repository, pool_repository: pool) }
- it 'removes the membership' do
- project.leave_pool_repository
+ subject { project.leave_pool_repository }
+
+ it 'removes the membership and disconnects alternates' do
+ expect(pool).to receive(:unlink_repository).with(project.repository, disconnect: true).and_call_original
+
+ subject
expect(pool.member_projects.reload).not_to include(project)
end
+
+ context 'when the project is pending delete' do
+ it 'removes the membership and does not disconnect alternates' do
+ project.pending_delete = true
+
+ expect(pool).to receive(:unlink_repository).with(project.repository, disconnect: false).and_call_original
+
+ subject
+
+ expect(pool.member_projects.reload).not_to include(project)
+ end
+ end
end
describe '#check_personal_projects_limit' do
@@ -8434,6 +8301,25 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#packages_policy_subject' do
+ let_it_be(:project) { create(:project) }
+
+ it 'returns wrapper' do
+ expect(project.packages_policy_subject).to be_a(Packages::Policies::Project)
+ expect(project.packages_policy_subject.project).to eq(project)
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(read_package_policy_rule: false)
+ end
+
+ it 'returns project' do
+ expect(project.packages_policy_subject).to eq(project)
+ end
+ end
+ end
+
describe '#destroy_deployment_by_id' do
let(:project) { create(:project, :repository) }
@@ -8451,6 +8337,25 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#can_create_custom_domains?' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pages_domain) { create(:pages_domain, project: project) }
+
+ subject { project.can_create_custom_domains? }
+
+ context 'when max custom domain setting is set to 0' do
+ it { is_expected.to be true }
+ end
+
+ context 'when max custom domain setting is not set to 0' do
+ before do
+ Gitlab::CurrentSettings.update!(max_pages_custom_domains_per_project: 1)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index f4edc68457b..b2158baa670 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -407,6 +407,25 @@ RSpec.describe ProjectStatistics do
end
end
+ describe '#refresh_storage_size!' do
+ it 'recalculates storage size from its components and save it' do
+ statistics.update_columns(
+ repository_size: 2,
+ wiki_size: 4,
+ lfs_objects_size: 3,
+ snippets_size: 2,
+ pipeline_artifacts_size: 3,
+ build_artifacts_size: 3,
+ packages_size: 6,
+ uploads_size: 5,
+
+ storage_size: 0
+ )
+
+ expect { statistics.refresh_storage_size! }.to change { statistics.storage_size }.from(0).to(28)
+ end
+ end
+
describe '.increment_statistic' do
shared_examples 'a statistic that increases storage_size' do
it 'increases the statistic by that amount' do
@@ -432,16 +451,15 @@ RSpec.describe ProjectStatistics do
end
end
- it 'schedules a worker to update the statistic and storage_size async' do
+ it 'schedules a worker to update the statistic and storage_size async', :sidekiq_inline do
expect(FlushCounterIncrementsWorker)
.to receive(:perform_in)
.with(CounterAttribute::WORKER_DELAY, described_class.name, statistics.id, stat)
+ .and_call_original
- expect(FlushCounterIncrementsWorker)
- .to receive(:perform_in)
- .with(CounterAttribute::WORKER_DELAY, described_class.name, statistics.id, :storage_size)
-
- described_class.increment_statistic(project, stat, 20)
+ expect { described_class.increment_statistic(project, stat, 20) }
+ .to change { statistics.reload.send(stat) }.by(20)
+ .and change { statistics.reload.send(:storage_size) }.by(20)
end
end
diff --git a/spec/models/projects/build_artifacts_size_refresh_spec.rb b/spec/models/projects/build_artifacts_size_refresh_spec.rb
index 052e654af76..21cd8e0b9d4 100644
--- a/spec/models/projects/build_artifacts_size_refresh_spec.rb
+++ b/spec/models/projects/build_artifacts_size_refresh_spec.rb
@@ -265,4 +265,16 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do
it { is_expected.to eq(result) }
end
end
+
+ describe 'callbacks' do
+ context 'when destroyed' do
+ it 'enqueues a Namespaces::ScheduleAggregationWorker' do
+ refresh = create(:project_build_artifacts_size_refresh)
+
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(refresh.project.namespace_id)
+
+ refresh.destroy!
+ end
+ end
+ end
end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 3936e7127b8..54a90ca6049 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -171,8 +171,8 @@ RSpec.describe ProtectedBranch do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:protected_branch) { create(:protected_branch, project: project, name: "“jawn”") }
- let(:feature_flag) { true }
- let(:dry_run) { true }
+ let(:use_new_cache_implementation) { true }
+ let(:rely_on_new_cache) { true }
shared_examples_for 'hash based cache implementation' do
it 'calls only hash based cache implementation' do
@@ -182,19 +182,22 @@ RSpec.describe ProtectedBranch do
expect(Rails.cache).not_to receive(:fetch)
- described_class.protected?(project, 'missing-branch', dry_run: dry_run)
+ described_class.protected?(project, 'missing-branch')
end
end
before do
- stub_feature_flags(hash_based_cache_for_protected_branches: feature_flag)
+ stub_feature_flags(hash_based_cache_for_protected_branches: use_new_cache_implementation)
+ stub_feature_flags(rely_on_protected_branches_cache: rely_on_new_cache)
allow(described_class).to receive(:matching).and_call_original
# the original call works and warms the cache
- described_class.protected?(project, protected_branch.name, dry_run: dry_run)
+ described_class.protected?(project, protected_branch.name)
end
- context 'Dry-run: true' do
+ context 'Dry-run: true (rely_on_protected_branches_cache is off, new hash-based is used)' do
+ let(:rely_on_new_cache) { false }
+
it 'recalculates a fresh value every time in order to check the cache is not returning stale data' do
expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).twice
@@ -204,21 +207,21 @@ RSpec.describe ProtectedBranch do
it_behaves_like 'hash based cache implementation'
end
- context 'Dry-run: false' do
- let(:dry_run) { false }
+ context 'Dry-run: false (rely_on_protected_branches_cache is enabled, new hash-based cache is used)' do
+ let(:rely_on_new_cache) { true }
it 'correctly invalidates a cache' do
expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).exactly(3).times.and_call_original
create_params = { name: 'bar', merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] }
branch = ProtectedBranches::CreateService.new(project, project.owner, create_params).execute
- expect(described_class.protected?(project, protected_branch.name, dry_run: dry_run)).to eq(true)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
ProtectedBranches::UpdateService.new(project, project.owner, name: 'ber').execute(branch)
- expect(described_class.protected?(project, protected_branch.name, dry_run: dry_run)).to eq(true)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
ProtectedBranches::DestroyService.new(project, project.owner).execute(branch)
- expect(described_class.protected?(project, protected_branch.name, dry_run: dry_run)).to eq(true)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
it_behaves_like 'hash based cache implementation'
@@ -229,7 +232,7 @@ RSpec.describe ProtectedBranch do
project.touch
- described_class.protected?(project, protected_branch.name, dry_run: dry_run)
+ described_class.protected?(project, protected_branch.name)
end
end
@@ -240,19 +243,19 @@ RSpec.describe ProtectedBranch do
another_project = create(:project)
ProtectedBranches::CreateService.new(another_project, another_project.owner, name: 'bar').execute
- described_class.protected?(project, protected_branch.name, dry_run: dry_run)
+ described_class.protected?(project, protected_branch.name)
end
end
it 'correctly uses the cached version' do
expect(described_class).not_to receive(:matching)
- expect(described_class.protected?(project, protected_branch.name, dry_run: dry_run)).to eq(true)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
end
context 'when feature flag hash_based_cache_for_protected_branches is off' do
- let(:feature_flag) { false }
+ let(:use_new_cache_implementation) { false }
it 'does not call hash based cache implementation' do
expect(ProtectedBranches::CacheService).not_to receive(:new)
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 429ad550626..adb4777ae90 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -359,10 +359,10 @@ RSpec.describe RemoteMirror, :mailer do
it 'resets all the columns when URL changes' do
remote_mirror.update!(last_error: Time.current,
- last_update_at: Time.current,
- last_successful_update_at: Time.current,
- update_status: 'started',
- error_notification_sent: true)
+ last_update_at: Time.current,
+ last_successful_update_at: Time.current,
+ update_status: 'started',
+ error_notification_sent: true)
expect { remote_mirror.update_attribute(:url, 'http://new.example.com') }
.to change { remote_mirror.last_error }.to(nil)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 47532ed1216..4e386bf584f 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -161,6 +161,33 @@ RSpec.describe Repository do
end
end
+ context 'semantic versioning sort' do
+ let(:version_two) { 'v2.0.0' }
+ let(:version_ten) { 'v10.0.0' }
+
+ before do
+ repository.add_tag(user, version_two, repository.commit.id)
+ repository.add_tag(user, version_ten, repository.commit.id)
+ end
+
+ after do
+ repository.rm_tag(user, version_two)
+ repository.rm_tag(user, version_ten)
+ end
+
+ context 'desc' do
+ subject { repository.tags_sorted_by('version_desc').map(&:name) & (tags_to_compare + [version_two, version_ten]) }
+
+ it { is_expected.to eq([version_ten, version_two, 'v1.1.0', 'v1.0.0']) }
+ end
+
+ context 'asc' do
+ subject { repository.tags_sorted_by('version_asc').map(&:name) & (tags_to_compare + [version_two, version_ten]) }
+
+ it { is_expected.to eq(['v1.0.0', 'v1.1.0', version_two, version_ten]) }
+ end
+ end
+
context 'unknown option' do
subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare }
@@ -518,6 +545,54 @@ RSpec.describe Repository do
end
end
+ describe '#list_commits_by' do
+ it 'returns commits with messages containing a given string' do
+ commit_ids = repository.list_commits_by('test text', 'master').map(&:id)
+
+ expect(commit_ids).to include(
+ 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+ '498214de67004b1da3d820901307bed2a68a8ef6'
+ )
+ expect(commit_ids).not_to include('c84ff944ff4529a70788a5e9003c2b7feae29047')
+ end
+
+ it 'is case insensitive' do
+ commit_ids = repository.list_commits_by('TEST TEXT', 'master').map(&:id)
+
+ expect(commit_ids).to include('b83d6e391c22777fca1ed3012fce84f633d7fed0')
+ end
+
+ it 'returns commits based in before filter' do
+ commit_ids = repository.list_commits_by('test text', 'master', before: 1474828200).map(&:id)
+ expect(commit_ids).to include(
+ '498214de67004b1da3d820901307bed2a68a8ef6'
+ )
+ expect(commit_ids).not_to include('b83d6e391c22777fca1ed3012fce84f633d7fed0')
+ end
+
+ it 'returns commits based in after filter' do
+ commit_ids = repository.list_commits_by('test text', 'master', after: 1474828200).map(&:id)
+ expect(commit_ids).to include(
+ 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
+ )
+ expect(commit_ids).not_to include('498214de67004b1da3d820901307bed2a68a8ef6')
+ end
+
+ it 'returns commits based in author filter' do
+ commit_ids = repository.list_commits_by('test text', 'master', author: 'Job van der Voort').map(&:id)
+ expect(commit_ids).to include(
+ 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
+ )
+ expect(commit_ids).not_to include('498214de67004b1da3d820901307bed2a68a8ef6')
+ end
+
+ describe 'when storage is broken', :broken_storage do
+ it 'raises a storage error' do
+ expect_to_raise_storage_error { broken_repository.list_commits_by('s') }
+ end
+ end
+ end
+
describe '#blob_at' do
context 'blank sha' do
subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }
@@ -3306,7 +3381,7 @@ RSpec.describe Repository do
before do
storages = {
'default' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories'),
- 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
+ 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
index 2b4898b750a..f84634bd220 100644
--- a/spec/models/resource_state_event_spec.rb
+++ b/spec/models/resource_state_event_spec.rb
@@ -42,16 +42,44 @@ RSpec.describe ResourceStateEvent, type: :model do
context 'callbacks' do
describe '#issue_usage_metrics' do
- it 'tracks closed issues' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_closed_action)
-
- create(described_class.name.underscore.to_sym, issue: issue, state: described_class.states[:closed])
+ describe 'when an issue is closed' do
+ subject(:close_issue) do
+ create(described_class.name.underscore.to_sym, issue: issue,
+ state: described_class.states[:closed])
+ end
+
+ it 'tracks closed issues' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_closed_action)
+
+ close_issue
+ end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CLOSED }
+ let(:project) { issue.project }
+ let(:user) { issue.author }
+ subject(:service_action) { close_issue }
+ end
end
- it 'tracks reopened issues' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_reopened_action)
+ describe 'when an issue is reopened' do
+ subject(:reopen_issue) do
+ create(described_class.name.underscore.to_sym, issue: issue,
+ state: described_class.states[:reopened])
+ end
+
+ it 'tracks reopened issues' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_reopened_action)
+
+ reopen_issue
+ end
- create(described_class.name.underscore.to_sym, issue: issue, state: described_class.states[:reopened])
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_REOPENED }
+ let(:project) { issue.project }
+ let(:user) { issue.author }
+ subject(:service_action) { reopen_issue }
+ end
end
it 'does not track merge requests' do
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index e8a933d2277..655cfad57c9 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe SnippetRepository do
let(:data) { [new_file, move_file, update_file] }
it 'returns nil when files argument is empty' do
- expect(snippet.repository).not_to receive(:multi_action)
+ expect(snippet.repository).not_to receive(:commit_files)
operation = snippet_repository.multi_files_action(user, [], **commit_opts)
@@ -47,7 +47,7 @@ RSpec.describe SnippetRepository do
end
it 'returns nil when files argument is nil' do
- expect(snippet.repository).not_to receive(:multi_action)
+ expect(snippet.repository).not_to receive(:commit_files)
operation = snippet_repository.multi_files_action(user, nil, **commit_opts)
@@ -119,7 +119,7 @@ RSpec.describe SnippetRepository do
end
it 'infers the commit action based on the parameters if not present' do
- expect(repo).to receive(:multi_action).with(user, hash_including(actions: result))
+ expect(repo).to receive(:commit_files).with(user, hash_including(actions: result))
snippet_repository.multi_files_action(user, data, **commit_opts)
end
@@ -131,7 +131,7 @@ RSpec.describe SnippetRepository do
specify do
expect(repo).to(
- receive(:multi_action).with(
+ receive(:commit_files).with(
user,
hash_including(actions: array_including(hash_including(action: expected_action)))))
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index 97a0dc27f17..a40c7c5c892 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -21,15 +21,37 @@ RSpec.describe SpamLog do
end
context 'when admin mode is enabled', :enable_admin_mode do
- it 'removes the user', :sidekiq_might_not_need_inline do
- spam_log = build(:spam_log)
- user = spam_log.user
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates user removal', :sidekiq_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
+
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
+ end
+
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ ).to be_exists
+ end
+ end
- perform_enqueued_jobs do
- spam_log.remove_user(deleted_by: admin)
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
end
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ it 'removes the user', :sidekiq_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
+
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
+ end
+
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 69cd51137b5..04f2c7f9176 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -334,6 +334,58 @@ RSpec.describe User do
end
end
end
+
+ context 'check_password_weakness' do
+ let(:weak_password) { "qwertyuiop" }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(block_weak_passwords: false)
+ end
+
+ it 'does not add an error when password is weak' do
+ expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
+
+ user.password = weak_password
+ expect(user).to be_valid
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(block_weak_passwords: true)
+ end
+
+ it 'checks for password weakness when password changes' do
+ expect(Security::WeakPasswords).to receive(:weak_for_user?)
+ .with(weak_password, user).and_call_original
+ user.password = weak_password
+ expect(user).not_to be_valid
+ end
+
+ it 'adds an error when password is weak' do
+ user.password = weak_password
+ expect(user).not_to be_valid
+ expect(user.errors).to be_of_kind(:password, 'must not contain commonly used combinations of words and letters')
+ end
+
+ it 'is valid when password is not weak' do
+ user.password = ::User.random_password
+ expect(user).to be_valid
+ end
+
+ it 'is valid when weak password was already set' do
+ user = build(:user, password: weak_password)
+ user.save!(validate: false)
+
+ expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
+
+ # Change an unrelated value
+ user.name = "Example McExampleFace"
+ expect(user).to be_valid
+ end
+ end
+ end
end
describe 'name' do
@@ -2071,11 +2123,12 @@ RSpec.describe User do
context 'user has existing U2F registration' do
it 'returns false' do
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- create(:u2f_registration, name: 'my u2f device',
- user: user,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: Base64.strict_encode64(device.origin_public_key_raw))
+ create(:u2f_registration,
+ name: 'my u2f device',
+ user: user,
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: Base64.strict_encode64(device.origin_public_key_raw))
expect(user.two_factor_u2f_enabled?).to eq(false)
end
@@ -2094,11 +2147,12 @@ RSpec.describe User do
context 'user has existing U2F registration' do
it 'returns true' do
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- create(:u2f_registration, name: 'my u2f device',
- user: user,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: Base64.strict_encode64(device.origin_public_key_raw))
+ create(:u2f_registration,
+ name: 'my u2f device',
+ user: user,
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: Base64.strict_encode64(device.origin_public_key_raw))
expect(user.two_factor_u2f_enabled?).to eq(true)
end
@@ -3601,15 +3655,15 @@ RSpec.describe User do
user = create :user
project = create(:project, :public)
- expect(user.starred?(project)).to be_falsey
-
- user.toggle_star(project)
-
- expect(user.starred?(project)).to be_truthy
-
- user.toggle_star(project)
+ # starring
+ expect { user.toggle_star(project) }
+ .to change { user.starred?(project) }.from(false).to(true)
+ .and not_change { project.reload.updated_at }
- expect(user.starred?(project)).to be_falsey
+ # unstarring
+ expect { user.toggle_star(project) }
+ .to change { user.starred?(project) }.from(true).to(false)
+ .and not_change { project.reload.updated_at }
end
end
@@ -3810,8 +3864,8 @@ RSpec.describe User do
describe '#can_be_deactivated?' do
let(:activity) { {} }
let(:user) { create(:user, name: 'John Smith', **activity) }
- let(:day_within_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.pred.days.ago }
- let(:day_outside_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.next.days.ago }
+ let(:day_within_minium_inactive_days_threshold) { Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago }
+ let(:day_outside_minium_inactive_days_threshold) { Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago }
shared_examples 'not eligible for deactivation' do
it 'returns false' do
@@ -7193,8 +7247,8 @@ RSpec.describe User do
describe '.dormant' do
it 'returns dormant users' do
freeze_time do
- not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date
- too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date
+ not_that_long_ago = (Gitlab::CurrentSettings.deactivate_dormant_users_period - 1).days.ago.to_date
+ too_long_ago = Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date
create(:user, :deactivated, last_activity_on: too_long_ago)
@@ -7214,8 +7268,8 @@ RSpec.describe User do
describe '.with_no_activity' do
it 'returns users with no activity' do
freeze_time do
- active_not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date
- active_too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date
+ active_not_that_long_ago = (Gitlab::CurrentSettings.deactivate_dormant_users_period - 1).days.ago.to_date
+ active_too_long_ago = Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date
created_recently = (described_class::MINIMUM_DAYS_CREATED - 1).days.ago.to_date
created_not_recently = described_class::MINIMUM_DAYS_CREATED.days.ago.to_date
@@ -7396,25 +7450,6 @@ RSpec.describe User do
let(:factory_name) { :user }
end
- describe 'mr_attention_requests_enabled?' do
- let(:user) { create(:user) }
-
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it { expect(user.mr_attention_requests_enabled?).to be(false) }
-
- it 'feature flag is enabled for user' do
- stub_feature_flags(mr_attention_requests: user)
-
- another_user = create(:user)
-
- expect(user.mr_attention_requests_enabled?).to be(true)
- expect(another_user.mr_attention_requests_enabled?).to be(false)
- end
- end
-
describe 'user age' do
let(:user) { create(:user, created_at: Date.yesterday) }
diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb
index 663df9712ab..289e1ce1856 100644
--- a/spec/models/user_status_spec.rb
+++ b/spec/models/user_status_spec.rb
@@ -18,6 +18,14 @@ RSpec.describe UserStatus do
expect { status.user.destroy! }.to change { described_class.count }.from(1).to(0)
end
+ describe '#clear_status_after' do
+ it 'is an alias of #clear_status_at', :freeze_time do
+ status = build(:user_status, clear_status_at: 8.hours.from_now)
+
+ expect(status.clear_status_after).to be_like_time(8.hours.from_now)
+ end
+ end
+
describe '#clear_status_after=' do
it 'sets clear_status_at' do
status = build(:user_status)
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 34cfd500c26..58b529ff18a 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -28,4 +28,27 @@ RSpec.describe Users::CreditCardValidation do
expect(subject.similar_records).to eq([match2, match1, subject])
end
end
+
+ describe '#similar_holder_names_count' do
+ subject!(:credit_card_validation) { create(:credit_card_validation, holder_name: holder_name) }
+
+ context 'when holder_name is present' do
+ let(:holder_name) { 'ALICE M SMITH' }
+
+ let!(:match) { create(:credit_card_validation, holder_name: 'Alice M Smith') }
+ let!(:non_match) { create(:credit_card_validation, holder_name: 'Bob B Brown') }
+
+ it 'returns the count of cards with similar case insensitive holder names' do
+ expect(subject.similar_holder_names_count).to eq(2)
+ end
+ end
+
+ context 'when holder_name is nil' do
+ let(:holder_name) { nil }
+
+ it 'returns 0' do
+ expect(subject.similar_holder_names_count).to eq(0)
+ end
+ end
+ end
end
diff --git a/spec/models/users/ghost_user_migration_spec.rb b/spec/models/users/ghost_user_migration_spec.rb
new file mode 100644
index 00000000000..d4a0657c3be
--- /dev/null
+++ b/spec/models/users/ghost_user_migration_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::GhostUserMigration do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:initiator_user) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:user_id) }
+ end
+end
diff --git a/spec/models/users/merge_request_interaction_spec.rb b/spec/models/users/merge_request_interaction_spec.rb
index a499a7c68e8..0b1888bd9a6 100644
--- a/spec/models/users/merge_request_interaction_spec.rb
+++ b/spec/models/users/merge_request_interaction_spec.rb
@@ -59,11 +59,8 @@ RSpec.describe ::Users::MergeRequestInteraction do
context 'when the user has been asked to review the MR' do
before do
merge_request.reviewers << user
- merge_request.find_reviewer(user).update!(state: :attention_requested)
end
- it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['ATTENTION_REQUESTED'].value) }
-
it 'implies not reviewed' do
expect(interaction).not_to be_reviewed
end
diff --git a/spec/models/users_star_project_spec.rb b/spec/models/users_star_project_spec.rb
new file mode 100644
index 00000000000..e41519a2b69
--- /dev/null
+++ b/spec/models/users_star_project_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe UsersStarProject, type: :model do
+ it { is_expected.to belong_to(:project).touch(false) }
+end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index e2240c225a9..341f9a9c60f 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -59,6 +59,14 @@ RSpec.describe WorkItem do
create(:work_item)
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:work_item) { create(:work_item) }
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CREATED }
+ let(:project) { work_item.project }
+ let(:user) { work_item.author }
+ subject(:service_action) { work_item }
+ end
end
context 'work item namespace' do
@@ -123,8 +131,8 @@ RSpec.describe WorkItem do
child.confidential = false
expect(child).not_to be_valid
- expect(child.errors[:confidential])
- .to include('associated parent is confidential and can not have non-confidential children.')
+ expect(child.errors[:base])
+ .to include(_('A non-confidential work item cannot have a confidential parent.'))
end
it 'allows to make parent non-confidential' do
@@ -143,8 +151,9 @@ RSpec.describe WorkItem do
parent.confidential = true
expect(parent).not_to be_valid
- expect(parent.errors[:confidential])
- .to include('confidential parent can not be used if there are non-confidential children.')
+ expect(parent.errors[:base]).to include(
+ _('A confidential work item cannot have a parent that already has non-confidential children.')
+ )
end
it 'allows to make child confidential' do
@@ -161,8 +170,8 @@ RSpec.describe WorkItem do
child.work_item_parent = create(:work_item, confidential: true, project: project)
expect(child).not_to be_valid
- expect(child.errors[:confidential])
- .to include('associated parent is confidential and can not have non-confidential children.')
+ expect(child.errors[:base])
+ .to include('A non-confidential work item cannot have a confidential parent.')
end
end
end
diff --git a/spec/models/work_items/widgets/description_spec.rb b/spec/models/work_items/widgets/description_spec.rb
index 8359db31bff..c24dc9cfb9c 100644
--- a/spec/models/work_items/widgets/description_spec.rb
+++ b/spec/models/work_items/widgets/description_spec.rb
@@ -3,7 +3,10 @@
require 'spec_helper'
RSpec.describe WorkItems::Widgets::Description do
- let_it_be(:work_item) { create(:work_item, description: '# Title') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:work_item, refind: true) do
+ create(:work_item, description: 'Title', last_edited_at: 10.days.ago, last_edited_by: user)
+ end
describe '.type' do
subject { described_class.type }
@@ -22,4 +25,42 @@ RSpec.describe WorkItems::Widgets::Description do
it { is_expected.to eq(work_item.description) }
end
+
+ describe '#edited?' do
+ subject { described_class.new(work_item).edited? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#last_edited_at' do
+ subject { described_class.new(work_item).last_edited_at }
+
+ it { is_expected.to eq(work_item.last_edited_at) }
+ end
+
+ describe '#last_edited_by' do
+ subject { described_class.new(work_item).last_edited_by }
+
+ context 'when the work item is edited' do
+ context 'when last edited user still exists in the DB' do
+ it { is_expected.to eq(user) }
+ end
+
+ context 'when last edited user no longer exists' do
+ before do
+ work_item.update!(last_edited_by: nil)
+ end
+
+ it { is_expected.to eq(User.ghost) }
+ end
+ end
+
+ context 'when the work item is not edited yet' do
+ before do
+ work_item.update!(last_edited_at: nil)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index f2c99e0de95..9aa50876b55 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineSchedulePolicy, :models do
+RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline_schedule, reload: true) { create(:ci_pipeline_schedule, :nightly, project: project) }
diff --git a/spec/policies/ci/runner_policy_spec.rb b/spec/policies/ci/runner_policy_spec.rb
new file mode 100644
index 00000000000..880ff0722fa
--- /dev/null
+++ b/spec/policies/ci/runner_policy_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerPolicy do
+ describe 'ability :read_runner' do
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+
+ let_it_be(:group1) { create(:group, name: 'top-level', path: 'top-level') }
+ let_it_be(:subgroup1) { create(:group, name: 'subgroup1', path: 'subgroup1', parent: group1) }
+ let_it_be(:project1) { create(:project, group: subgroup1) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:group1_runner) { create(:ci_runner, :group, groups: [group1]) }
+ let_it_be(:project1_runner) { create(:ci_runner, :project, projects: [project1]) }
+
+ subject(:policy) { described_class.new(user, runner) }
+
+ before do
+ group1.add_guest(guest)
+ group1.add_developer(developer)
+ group1.add_owner(owner)
+ end
+
+ shared_context 'on hierarchy with shared runners disabled' do
+ around do |example|
+ group1.update!(shared_runners_enabled: false)
+ project1.update!(shared_runners_enabled: false)
+
+ example.run
+ ensure
+ project1.update!(shared_runners_enabled: true)
+ group1.update!(shared_runners_enabled: true)
+ end
+ end
+
+ shared_context 'on hierarchy with group runners disabled' do
+ around do |example|
+ project1.update!(group_runners_enabled: false)
+
+ example.run
+ ensure
+ project1.update!(group_runners_enabled: true)
+ end
+ end
+
+ shared_examples 'does not allow reading runners on any scope' do
+ context 'with instance runner' do
+ let(:runner) { instance_runner }
+
+ it { expect_disallowed :read_runner }
+
+ context 'with shared runners disabled' do
+ include_context 'on hierarchy with shared runners disabled' do
+ it { expect_disallowed :read_runner }
+ end
+ end
+ end
+
+ context 'with group runner' do
+ let(:runner) { group1_runner }
+
+ it { expect_disallowed :read_runner }
+
+ context 'with group runner disabled' do
+ include_context 'on hierarchy with group runners disabled' do
+ it { expect_disallowed :read_runner }
+ end
+ end
+ end
+
+ context 'with project runner' do
+ let(:runner) { project1_runner }
+
+ it { expect_disallowed :read_runner }
+ end
+ end
+
+ context 'without access' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'does not allow reading runners on any scope'
+ end
+
+ context 'with guest access' do
+ let(:user) { guest }
+
+ it_behaves_like 'does not allow reading runners on any scope'
+ end
+
+ context 'with developer access' do
+ let(:user) { developer }
+
+ context 'with instance runner' do
+ let(:runner) { instance_runner }
+
+ it { expect_allowed :read_runner }
+
+ context 'with shared runners disabled' do
+ include_context 'on hierarchy with shared runners disabled' do
+ it { expect_disallowed :read_runner }
+ end
+ end
+ end
+
+ context 'with group runner' do
+ let(:runner) { group1_runner }
+
+ it { expect_allowed :read_runner }
+
+ context 'with group runner disabled' do
+ include_context 'on hierarchy with group runners disabled' do
+ it { expect_disallowed :read_runner }
+ end
+ end
+ end
+
+ context 'with project runner' do
+ let(:runner) { project1_runner }
+
+ it { expect_disallowed :read_runner }
+ end
+ end
+
+ context 'with owner access' do
+ let(:user) { owner }
+
+ context 'with instance runner' do
+ let(:runner) { instance_runner }
+
+ context 'with shared runners disabled' do
+ include_context 'on hierarchy with shared runners disabled' do
+ it { expect_disallowed :read_runner }
+ end
+ end
+
+ it { expect_allowed :read_runner }
+ end
+
+ context 'with group runner' do
+ let(:runner) { group1_runner }
+
+ context 'with group runners disabled' do
+ include_context 'on hierarchy with group runners disabled' do
+ it { expect_allowed :read_runner }
+ end
+ end
+
+ it { expect_allowed :read_runner }
+ end
+
+ context 'with project runner' do
+ let(:runner) { project1_runner }
+
+ it { expect_allowed :read_runner }
+ end
+ end
+ end
+end
diff --git a/spec/policies/clusters/agent_policy_spec.rb b/spec/policies/clusters/agent_policy_spec.rb
index 307d751b78b..8f778d318ed 100644
--- a/spec/policies/clusters/agent_policy_spec.rb
+++ b/spec/policies/clusters/agent_policy_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Clusters::AgentPolicy do
- let(:cluster_agent) { create(:cluster_agent, name: 'agent' )}
+ let(:cluster_agent) { create(:cluster_agent, name: 'agent' ) }
let(:user) { create(:admin) }
let(:policy) { described_class.new(user, cluster_agent) }
let(:project) { cluster_agent.project }
diff --git a/spec/policies/commit_policy_spec.rb b/spec/policies/commit_policy_spec.rb
index 0d3dcc97565..cf2798b9ef3 100644
--- a/spec/policies/commit_policy_spec.rb
+++ b/spec/policies/commit_policy_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe CommitPolicy do
describe '#rules' do
+ let(:group) { create(:group, :public) }
let(:user) { create(:user) }
let(:commit) { project.repository.head_commit }
let(:policy) { described_class.new(user, commit) }
@@ -19,59 +20,119 @@ RSpec.describe CommitPolicy do
end
shared_examples 'cannot read commit nor create a note' do
- it 'can not read commit' do
+ it 'cannot read commit' do
expect(policy).to be_disallowed(:read_commit)
end
- it 'can not create a note' do
+ it 'cannot create a note' do
expect(policy).to be_disallowed(:create_note)
end
end
context 'when project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let(:project) { create(:project, :public, :repository, group: group) }
- it_behaves_like 'can read commit and create a note'
+ context 'when the user is not a project member' do
+ it_behaves_like 'can read commit and create a note'
+ end
context 'when repository access level is private' do
- let(:project) { create(:project, :public, :repository, :repository_private) }
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- it_behaves_like 'cannot read commit nor create a note'
+ context 'when the user is not a project member' do
+ it_behaves_like 'cannot read commit nor create a note'
+ end
- context 'when the user is a project member' do
- before do
- project.add_developer(user)
+ context 'when the user is a direct project member' do
+ context 'and the user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
end
+ end
- it_behaves_like 'can read commit and create a note'
+ context 'when the user is an inherited member from the group' do
+ context 'and the user is a guest' do
+ before do
+ group.add_guest(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
+ end
+
+ context 'and the user is a reporter' do
+ before do
+ group.add_reporter(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
+ end
+
+ context 'and the user is a developer' do
+ before do
+ group.add_developer(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
+ end
end
end
end
context 'when project is private' do
- let(:project) { create(:project, :private, :repository) }
+ let(:project) { create(:project, :private, :repository, group: group) }
- it_behaves_like 'cannot read commit nor create a note'
+ context 'when the user is not a project member' do
+ it_behaves_like 'cannot read commit nor create a note'
+ end
- context 'when the user is a project member' do
- before do
- project.add_developer(user)
+ context 'when the user is a direct project member' do
+ context 'and the user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
end
- it 'can read commit and create a note' do
- expect(policy).to be_allowed(:read_commit)
+ context 'and the user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'cannot read commit nor create a note'
+
+ it 'cannot download code' do
+ expect(policy).to be_disallowed(:download_code)
+ end
end
end
- context 'when the user is a guest' do
- before do
- project.add_guest(user)
+ context 'when the user is an inherited member from the group' do
+ context 'and the user is a guest' do
+ before do
+ group.add_guest(user)
+ end
+
+ it_behaves_like 'cannot read commit nor create a note'
end
- it_behaves_like 'cannot read commit nor create a note'
+ context 'and the user is a reporter' do
+ before do
+ group.add_reporter(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
+ end
- it 'cannot download code' do
- expect(policy).to be_disallowed(:download_code)
+ context 'and the user is a developer' do
+ before do
+ group.add_developer(user)
+ end
+
+ it_behaves_like 'can read commit and create a note'
end
end
end
diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb
index 50774313aae..27ce683861c 100644
--- a/spec/policies/group_member_policy_spec.rb
+++ b/spec/policies/group_member_policy_spec.rb
@@ -128,7 +128,7 @@ RSpec.describe GroupMemberPolicy do
context 'with the group parent' do
let(:current_user) { create :user }
- let(:subgroup) { create(:group, :private, parent: group)}
+ let(:subgroup) { create(:group, :private, parent: group) }
before do
group.add_owner(owner)
@@ -143,7 +143,7 @@ RSpec.describe GroupMemberPolicy do
context 'without group parent' do
let(:current_user) { create :user }
- let(:subgroup) { create(:group, :private)}
+ let(:subgroup) { create(:group, :private) }
before do
subgroup.add_owner(current_user)
@@ -158,7 +158,7 @@ RSpec.describe GroupMemberPolicy do
context 'without group parent with two owners' do
let(:current_user) { create :user }
let(:other_user) { create :user }
- let(:subgroup) { create(:group, :private)}
+ let(:subgroup) { create(:group, :private) }
before do
subgroup.add_owner(current_user)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 57923142648..da0270c15b9 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe GroupPolicy do
let(:group) { create(:group, :public, :crm_enabled) }
let(:current_user) { nil }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
@@ -24,7 +24,7 @@ RSpec.describe GroupPolicy do
let(:group) { create(:group, :public, :crm_enabled) }
let(:current_user) { create(:user) }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
@@ -43,7 +43,7 @@ RSpec.describe GroupPolicy do
create(:project_group_link, project: project, group: group)
end
- it do
+ specify do
expect_disallowed(*public_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*owner_permissions)
@@ -58,7 +58,7 @@ RSpec.describe GroupPolicy do
create(:project_group_link, project: project, group: group)
end
- it do
+ specify do
expect_disallowed(*public_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*owner_permissions)
@@ -91,7 +91,7 @@ RSpec.describe GroupPolicy do
let(:deploy_token) { create(:deploy_token) }
let(:current_user) { deploy_token }
- it do
+ specify do
expect_disallowed(*public_permissions)
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
@@ -104,7 +104,7 @@ RSpec.describe GroupPolicy do
context 'guests' do
let(:current_user) { guest }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
@@ -121,7 +121,7 @@ RSpec.describe GroupPolicy do
context 'reporter' do
let(:current_user) { reporter }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -138,7 +138,7 @@ RSpec.describe GroupPolicy do
context 'developer' do
let(:current_user) { developer }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -195,7 +195,7 @@ RSpec.describe GroupPolicy do
context 'owner' do
let(:current_user) { owner }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -282,7 +282,7 @@ RSpec.describe GroupPolicy do
context 'with no user' do
let(:current_user) { nil }
- it do
+ specify do
expect_disallowed(*public_permissions)
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
@@ -295,7 +295,7 @@ RSpec.describe GroupPolicy do
context 'guests' do
let(:current_user) { guest }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
@@ -308,7 +308,7 @@ RSpec.describe GroupPolicy do
context 'reporter' do
let(:current_user) { reporter }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -321,7 +321,7 @@ RSpec.describe GroupPolicy do
context 'developer' do
let(:current_user) { developer }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -334,7 +334,7 @@ RSpec.describe GroupPolicy do
context 'maintainer' do
let(:current_user) { maintainer }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -347,7 +347,7 @@ RSpec.describe GroupPolicy do
context 'owner' do
let(:current_user) { owner }
- it do
+ specify do
expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@@ -916,6 +916,74 @@ RSpec.describe GroupPolicy do
end
end
+ describe 'observability' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:allowed) { be_allowed(:read_observability) }
+ let(:disallowed) { be_disallowed(:read_observability) }
+
+ # rubocop:disable Layout/LineLength
+ where(:feature_enabled, :admin_matcher, :owner_matcher, :maintainer_matcher, :developer_matcher, :reporter_matcher, :guest_matcher, :non_member_matcher, :anonymous_matcher) do
+ false | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed)
+ true | ref(:allowed) | ref(:allowed) | ref(:allowed) | ref(:allowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed)
+ end
+ # rubocop:enable Layout/LineLength
+
+ with_them do
+ before do
+ stub_feature_flags(observability_group_tab: feature_enabled)
+ end
+
+ context 'admin', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it { is_expected.to admin_matcher }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to owner_matcher }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to maintainer_matcher }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to developer_matcher }
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to reporter_matcher }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to guest_matcher }
+ end
+
+ context 'with non member' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to non_member_matcher }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to anonymous_matcher }
+ end
+ end
+ end
+
describe 'dependency proxy' do
context 'feature disabled' do
let(:current_user) { owner }
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
index 706570babd5..fd7ec5917d6 100644
--- a/spec/policies/issuable_policy_spec.rb
+++ b/spec/policies/issuable_policy_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe IssuablePolicy, models: true do
project.add_reporter(reporter)
end
- def permissions(user, issue)
- described_class.new(user, issue)
+ def permissions(user, issuable)
+ described_class.new(user, issuable)
end
describe '#rules' do
@@ -153,5 +153,55 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(reporter, issue)).to be_allowed(:create_timelog)
end
end
+
+ context 'when subject is a Merge Request' do
+ let(:issuable) { create(:merge_request) }
+ let(:policy) { permissions(user, issuable) }
+
+ before do
+ allow(policy).to receive(:can?).with(:read_merge_request).and_return(can_read_merge_request)
+ end
+
+ context 'when can_read_merge_request is false' do
+ let(:can_read_merge_request) { false }
+
+ it 'does not allow :read_issuable' do
+ expect(policy).not_to be_allowed(:read_issuable)
+ end
+ end
+
+ context 'when can_read_merge_request is true' do
+ let(:can_read_merge_request) { true }
+
+ it 'allows :read_issuable' do
+ expect(policy).to be_allowed(:read_issuable)
+ end
+ end
+ end
+
+ context 'when subject is an Issue' do
+ let(:issuable) { create(:issue) }
+ let(:policy) { permissions(user, issuable) }
+
+ before do
+ allow(policy).to receive(:can?).with(:read_issue).and_return(can_read_issue)
+ end
+
+ context 'when can_read_issue is false' do
+ let(:can_read_issue) { false }
+
+ it 'does not allow :read_issuable' do
+ expect(policy).not_to be_allowed(:read_issuable)
+ end
+ end
+
+ context 'when can_read_issue is true' do
+ let(:can_read_issue) { true }
+
+ it 'allows :read_issuable' do
+ expect(policy).to be_allowed(:read_issuable)
+ end
+ end
+ end
end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 7ca4baddb79..4d492deb54c 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -398,7 +398,7 @@ RSpec.describe IssuePolicy do
context 'with a hidden issue' do
let(:user) { create(:user) }
let(:banned_user) { create(:user, :banned) }
- let(:admin) { create(:user, :admin)}
+ let(:admin) { create(:user, :admin) }
let(:hidden_issue) { create(:issue, project: project, author: banned_user) }
it 'does not allow non-admin user to read the issue' do
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index dd42e1b9313..7e1af132b1d 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -7,24 +7,18 @@ RSpec.describe MergeRequestPolicy do
let_it_be(:guest) { create(:user) }
let_it_be(:author) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:non_team_member) { create(:user) }
- let(:project) { create(:project, :public) }
-
def permissions(user, merge_request)
described_class.new(user, merge_request)
end
- before do
- project.add_guest(guest)
- project.add_guest(author)
- project.add_developer(developer)
- end
-
mr_perms = %i[create_merge_request_in
create_merge_request_from
read_merge_request
+ update_merge_request
create_todo
approve_merge_request
create_note
@@ -40,7 +34,28 @@ RSpec.describe MergeRequestPolicy do
end
end
- shared_examples_for 'a user with access' do
+ shared_examples_for 'a user with reporter access' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:policy, :is_allowed) do
+ :create_merge_request_in | true
+ :read_merge_request | true
+ :create_todo | true
+ :create_note | true
+ :update_subscription | true
+ :create_merge_request_from | false
+ :approve_merge_request | false
+ :update_merge_request | false
+ end
+
+ with_them do
+ specify do
+ is_allowed ? (is_expected.to be_allowed(policy)) : (is_expected.to be_disallowed(policy))
+ end
+ end
+ end
+
+ shared_examples_for 'a user with full access' do
let(:perms) { permissions(subject, merge_request) }
mr_perms.each do |thing|
@@ -50,199 +65,304 @@ RSpec.describe MergeRequestPolicy do
end
end
- context 'when merge request is public' do
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
- let(:user) { author }
-
- context 'and user is anonymous' do
- subject { permissions(nil, merge_request) }
+ context 'when user is a direct project member' do
+ let(:project) { create(:project, :public) }
- it do
- is_expected.to be_disallowed(:create_todo, :update_subscription)
- end
+ before do
+ project.add_guest(guest)
+ project.add_guest(author)
+ project.add_developer(developer)
end
- context 'and user is author' do
- subject { permissions(user, merge_request) }
+ context 'when merge request is public' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ let(:user) { author }
- context 'and the user is a guest' do
- let(:user) { guest }
+ context 'and user is author' do
+ subject { permissions(user, merge_request) }
- it do
- is_expected.to be_allowed(:update_merge_request)
- end
+ context 'and the user is a guest' do
+ let(:user) { guest }
- it do
- is_expected.to be_allowed(:reopen_merge_request)
- end
+ it do
+ is_expected.to be_allowed(:update_merge_request)
+ end
- it do
- is_expected.to be_allowed(:approve_merge_request)
+ it do
+ is_expected.to be_allowed(:reopen_merge_request)
+ end
+
+ it do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
end
end
+ end
- context 'and the user is a group member' do
- let(:project) { create(:project, :public, group: group) }
- let(:group) { create(:group) }
- let(:user) { non_team_member }
-
- before do
- group.add_guest(non_team_member)
- end
+ context 'when merge requests have been disabled' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
- it do
- is_expected.to be_allowed(:approve_merge_request)
- end
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
end
- context 'and the user is a member of a shared group' do
- let(:user) { non_team_member }
+ describe 'the author' do
+ subject { author }
- before do
- group = create(:group)
- project.project_group_links.create!(
- group: group,
- group_access: Gitlab::Access::DEVELOPER)
+ it_behaves_like 'a denied user'
+ end
- group.add_guest(non_team_member)
- end
+ describe 'a guest' do
+ subject { guest }
- it do
- is_expected.to be_allowed(:approve_merge_request)
- end
+ it_behaves_like 'a denied user'
end
- context 'and the user is not a project member' do
- let(:user) { non_team_member }
+ describe 'a developer' do
+ subject { developer }
- it do
- is_expected.not_to be_allowed(:approve_merge_request)
- end
+ it_behaves_like 'a denied user'
end
end
- end
- context 'when merge requests have been disabled' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
+ context 'when merge requests are private' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
- before do
- project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
- end
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
- describe 'the author' do
- subject { author }
+ describe 'the author' do
+ subject { author }
- it_behaves_like 'a denied user'
+ it_behaves_like 'a denied user'
+ end
+
+ describe 'a developer' do
+ subject { developer }
+
+ it_behaves_like 'a user with full access'
+ end
end
- describe 'a guest' do
- subject { guest }
+ context 'when merge request is unlocked' do
+ let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) }
- it_behaves_like 'a denied user'
+ it 'allows author to reopen merge request' do
+ expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request)
+ end
+
+ it 'allows developer to reopen merge request' do
+ expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request)
+ end
+
+ it 'prevents guest from reopening merge request' do
+ expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request)
+ end
end
- describe 'a developer' do
- subject { developer }
+ context 'when merge request is locked' do
+ let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) }
- it_behaves_like 'a denied user'
+ it 'prevents author from reopening merge request' do
+ expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ end
+
+ it 'prevents developer from reopening merge request' do
+ expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ end
+
+ it 'prevents guests from reopening merge request' do
+ expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ end
+
+ context 'when the user is project member, with at least guest access' do
+ let(:user) { guest }
+
+ it 'can create a note' do
+ expect(permissions(user, merge_request_locked)).to be_allowed(:create_note)
+ end
+ end
end
- describe 'any other user' do
- subject { non_team_member }
+ context 'with external authorization enabled' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:policies) { described_class.new(user, merge_request) }
- it_behaves_like 'a denied user'
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'can read the issue iid without accessing the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(policies).to be_allowed(:read_merge_request_iid)
+ end
end
end
- context 'when merge requests are private' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
+ context 'when user is an inherited member from the parent group' do
+ let_it_be(:group) { create(:group, :public) }
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: author) }
+
+ before_all do
+ group.add_guest(guest)
+ group.add_guest(author)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
end
- describe 'a non-team-member' do
- subject { non_team_member }
+ context 'when project is public' do
+ let(:project) { create(:project, :public, group: group) }
- it_behaves_like 'a denied user'
- end
+ describe 'the merge request author' do
+ subject { permissions(author, merge_request) }
- describe 'the author' do
- subject { author }
+ specify do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
+ end
- it_behaves_like 'a denied user'
+ context 'and merge requests are private' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ describe 'a guest' do
+ subject { guest }
+
+ it_behaves_like 'a denied user'
+ end
+
+ describe 'a reporter' do
+ subject { permissions(reporter, merge_request) }
+
+ it_behaves_like 'a user with reporter access'
+ end
+
+ describe 'a developer' do
+ subject { developer }
+
+ it_behaves_like 'a user with full access'
+ end
+ end
end
- describe 'a developer' do
- subject { developer }
+ context 'when project is private' do
+ let(:project) { create(:project, :private, group: group) }
+
+ describe 'a guest' do
+ subject { guest }
- it_behaves_like 'a user with access'
+ it_behaves_like 'a denied user'
+ end
+
+ describe 'a reporter' do
+ subject { permissions(reporter, merge_request) }
+
+ it_behaves_like 'a user with reporter access'
+ end
+
+ describe 'a developer' do
+ subject { developer }
+
+ it_behaves_like 'a user with full access'
+ end
end
end
- context 'when merge request is unlocked' do
- let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) }
+ context 'when user is an inherited member from a shared group' do
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ let(:user) { author }
- it 'allows author to reopen merge request' do
- expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request)
+ before do
+ project.add_guest(author)
end
- it 'allows developer to reopen merge request' do
- expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request)
- end
+ context 'and group is given developer access' do
+ let(:user) { non_team_member }
+
+ subject { permissions(user, merge_request) }
+
+ before do
+ group = create(:group)
+ project.project_group_links.create!(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER)
+
+ group.add_guest(non_team_member)
+ end
- it 'prevents guest from reopening merge request' do
- expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request)
+ specify do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
end
end
- context 'when merge request is locked' do
- let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) }
+ context 'when user is not a project member' do
+ let(:project) { create(:project, :public) }
- it 'prevents author from reopening merge request' do
- expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request)
- end
+ context 'when merge request is public' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- it 'prevents developer from reopening merge request' do
- expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ subject { permissions(non_team_member, merge_request) }
+
+ specify do
+ is_expected.not_to be_allowed(:approve_merge_request)
+ end
end
- it 'prevents guests from reopening merge request' do
- expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ context 'when merge requests are disabled' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+ end
+
+ subject { non_team_member }
+
+ it_behaves_like 'a denied user'
end
- context 'when the user is not a project member' do
- let(:user) { create(:user) }
+ context 'when merge requests are private' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- it 'cannot create a note' do
- expect(permissions(user, merge_request_locked)).to be_disallowed(:create_note)
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
end
+
+ subject { non_team_member }
+
+ it_behaves_like 'a denied user'
end
- context 'when the user is project member, with at least guest access' do
- let(:user) { guest }
+ context 'when merge request is locked' do
+ let(:merge_request) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project) }
- it 'can create a note' do
- expect(permissions(user, merge_request_locked)).to be_allowed(:create_note)
+ it 'cannot create a note' do
+ expect(permissions(non_team_member, merge_request)).to be_disallowed(:create_note)
end
end
end
- context 'with external authorization enabled' do
- let(:user) { create(:user) }
+ context 'when user is anonymous' do
let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, source_project: project) }
- let(:policies) { described_class.new(user, merge_request) }
- before do
- enable_external_authorization_service_check
- end
+ context 'when merge request is public' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- it 'can read the issue iid without accessing the external service' do
- expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+ subject { permissions(nil, merge_request) }
- expect(policies).to be_allowed(:read_merge_request_iid)
+ specify do
+ is_expected.to be_disallowed(:create_todo, :update_subscription)
+ end
end
end
end
diff --git a/spec/policies/packages/policies/group_policy_spec.rb b/spec/policies/packages/policies/group_policy_spec.rb
new file mode 100644
index 00000000000..d0d9a9a22f5
--- /dev/null
+++ b/spec/policies/packages/policies/group_policy_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Policies::GroupPolicy do
+ include_context 'GroupPolicy context'
+
+ subject { described_class.new(current_user, group.packages_policy_subject) }
+
+ describe 'read_package' do
+ context 'with admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:read_package) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:read_package) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:read_package) }
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:read_package) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:read_package) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:read_package) }
+ end
+
+ context 'with non member' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_disallowed(:read_package) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:read_package) }
+ end
+ end
+
+ describe 'deploy token access' do
+ let!(:group_deploy_token) do
+ create(:group_deploy_token, group: group, deploy_token: deploy_token)
+ end
+
+ subject { described_class.new(deploy_token, group.packages_policy_subject) }
+
+ context 'when a deploy token with read_package_registry scope' do
+ let(:deploy_token) { create(:deploy_token, :group, read_package_registry: true) }
+
+ it { is_expected.to be_allowed(:read_package) }
+ end
+
+ context 'when a deploy token with write_package_registry scope' do
+ let(:deploy_token) { create(:deploy_token, :group, write_package_registry: true) }
+
+ it { is_expected.to be_allowed(:read_package) }
+ end
+ end
+end
diff --git a/spec/policies/packages/policies/project_policy_spec.rb b/spec/policies/packages/policies/project_policy_spec.rb
new file mode 100644
index 00000000000..5d54ee54572
--- /dev/null
+++ b/spec/policies/packages/policies/project_policy_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Policies::ProjectPolicy do
+ include_context 'ProjectPolicy context'
+
+ let(:project) { public_project }
+
+ subject { described_class.new(current_user, project.packages_policy_subject) }
+
+ describe 'deploy token access' do
+ let!(:project_deploy_token) do
+ create(:project_deploy_token, project: project, deploy_token: deploy_token)
+ end
+
+ subject { described_class.new(deploy_token, project.packages_policy_subject) }
+
+ context 'when a deploy token with read_package_registry scope' do
+ let(:deploy_token) { create(:deploy_token, read_package_registry: true) }
+
+ it { is_expected.to be_allowed(:read_package) }
+
+ it_behaves_like 'package access with repository disabled'
+ end
+
+ context 'when a deploy token with write_package_registry scope' do
+ let(:deploy_token) { create(:deploy_token, write_package_registry: true) }
+
+ it { is_expected.to be_allowed(:read_package) }
+
+ it_behaves_like 'package access with repository disabled'
+ end
+ end
+
+ describe 'read_package', :enable_admin_mode do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project, :package_registry_access_level, :current_user, :expect_to_be_allowed) do
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:anonymous) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:non_member) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:guest) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:reporter) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:developer) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:maintainer) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:owner) | false
+ ref(:private_project) | ProjectFeature::DISABLED | ref(:admin) | false
+
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:anonymous) | false
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:non_member) | false
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:guest) | false
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:reporter) | true
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:developer) | true
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:maintainer) | true
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:owner) | true
+ ref(:private_project) | ProjectFeature::PRIVATE | ref(:admin) | true
+
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:anonymous) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:non_member) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:guest) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:reporter) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:developer) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:maintainer) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:owner) | true
+ ref(:private_project) | ProjectFeature::PUBLIC | ref(:admin) | true
+
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:anonymous) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:non_member) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:guest) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:reporter) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:developer) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:maintainer) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:owner) | false
+ ref(:internal_project) | ProjectFeature::DISABLED | ref(:admin) | false
+
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:anonymous) | false
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:non_member) | true
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:guest) | true
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:reporter) | true
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:developer) | true
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:maintainer) | true
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:owner) | true
+ ref(:internal_project) | ProjectFeature::ENABLED | ref(:admin) | true
+
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:anonymous) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:non_member) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:guest) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:reporter) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:developer) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:maintainer) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:owner) | true
+ ref(:internal_project) | ProjectFeature::PUBLIC | ref(:admin) | true
+
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:anonymous) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:non_member) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:guest) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:reporter) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:developer) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:maintainer) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:owner) | false
+ ref(:public_project) | ProjectFeature::DISABLED | ref(:admin) | false
+
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:anonymous) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:non_member) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:guest) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:reporter) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:developer) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:maintainer) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:owner) | true
+ ref(:public_project) | ProjectFeature::PUBLIC | ref(:admin) | true
+ end
+
+ with_them do
+ it do
+ project.project_feature.update!(package_registry_access_level: package_registry_access_level)
+
+ if expect_to_be_allowed
+ is_expected.to be_allowed(:read_package)
+ else
+ is_expected.to be_disallowed(:read_package)
+ end
+ end
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(package_registry_access_level: false)
+ end
+
+ where(:project, :current_user, :expect_to_be_allowed) do
+ ref(:private_project) | ref(:anonymous) | false
+ ref(:private_project) | ref(:non_member) | false
+ ref(:private_project) | ref(:guest) | false
+ ref(:internal_project) | ref(:anonymous) | false
+ ref(:public_project) | ref(:admin) | true
+ ref(:public_project) | ref(:owner) | true
+ ref(:public_project) | ref(:maintainer) | true
+ ref(:public_project) | ref(:developer) | true
+ ref(:public_project) | ref(:reporter) | true
+ ref(:public_project) | ref(:guest) | true
+ ref(:public_project) | ref(:non_member) | true
+ ref(:public_project) | ref(:anonymous) | true
+ end
+
+ with_them do
+ it do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+
+ if expect_to_be_allowed
+ is_expected.to be_allowed(:read_package)
+ else
+ is_expected.to be_disallowed(:read_package)
+ end
+ end
+ end
+ end
+
+ context 'with admin' do
+ let(:current_user) { admin }
+
+ it_behaves_like 'package access with repository disabled'
+ end
+ end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index e8fdf9a8e25..fefd9f71408 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -777,13 +777,13 @@ RSpec.describe ProjectPolicy do
project.add_developer(user)
end
- it { is_expected.not_to be_allowed(:project_bot_access)}
+ it { is_expected.not_to be_allowed(:project_bot_access) }
end
context "when project bot and not part of the project" do
let(:current_user) { project_bot }
- it { is_expected.not_to be_allowed(:project_bot_access)}
+ it { is_expected.not_to be_allowed(:project_bot_access) }
end
context "when project bot and part of the project" do
@@ -793,7 +793,7 @@ RSpec.describe ProjectPolicy do
project.add_developer(project_bot)
end
- it { is_expected.to be_allowed(:project_bot_access)}
+ it { is_expected.to be_allowed(:project_bot_access) }
end
end
@@ -804,7 +804,7 @@ RSpec.describe ProjectPolicy do
project.add_maintainer(project_bot)
end
- it { is_expected.not_to be_allowed(:create_resource_access_tokens)}
+ it { is_expected.not_to be_allowed(:create_resource_access_tokens) }
end
end
@@ -946,7 +946,7 @@ RSpec.describe ProjectPolicy do
context 'with anonymous' do
let(:current_user) { anonymous }
- it { is_expected.to be_disallowed(:metrics_dashboard)}
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
end
end
@@ -1930,14 +1930,10 @@ RSpec.describe ProjectPolicy do
describe 'operations feature' do
using RSpec::Parameterized::TableSyntax
- before do
- stub_feature_flags(split_operations_visibility_permissions: false)
- end
+ let(:guest_permissions) { [:read_environment, :read_deployment] }
- let(:guest_operations_permissions) { [:read_environment, :read_deployment] }
-
- let(:developer_operations_permissions) do
- guest_operations_permissions + [
+ let(:developer_permissions) do
+ guest_permissions + [
:read_feature_flag, :read_sentry_issue, :read_alert_management_alert, :read_terraform_state,
:metrics_dashboard, :read_pod_logs, :read_prometheus, :create_feature_flag,
:create_environment, :create_deployment, :update_feature_flag, :update_environment,
@@ -1946,13 +1942,17 @@ RSpec.describe ProjectPolicy do
]
end
- let(:maintainer_operations_permissions) do
- developer_operations_permissions + [
+ let(:maintainer_permissions) do
+ developer_permissions + [
:read_cluster, :create_cluster, :update_cluster, :admin_environment,
:admin_cluster, :admin_terraform_state, :admin_deployment
]
end
+ before do
+ stub_feature_flags(split_operations_visibility_permissions: false)
+ end
+
where(:project_visibility, :access_level, :role, :allowed) do
:public | ProjectFeature::ENABLED | :maintainer | true
:public | ProjectFeature::ENABLED | :developer | true
@@ -2005,33 +2005,22 @@ RSpec.describe ProjectPolicy do
expect_disallowed(*permissions_abilities(role))
end
end
-
- def permissions_abilities(role)
- case role
- when :maintainer
- maintainer_operations_permissions
- when :developer
- developer_operations_permissions
- else
- guest_operations_permissions
- end
- end
end
end
describe 'environments feature' do
using RSpec::Parameterized::TableSyntax
- let(:guest_environments_permissions) { [:read_environment, :read_deployment] }
+ let(:guest_permissions) { [:read_environment, :read_deployment] }
- let(:developer_environments_permissions) do
- guest_environments_permissions + [
+ let(:developer_permissions) do
+ guest_permissions + [
:create_environment, :create_deployment, :update_environment, :update_deployment, :destroy_environment
]
end
- let(:maintainer_environments_permissions) do
- developer_environments_permissions + [:admin_environment, :admin_deployment]
+ let(:maintainer_permissions) do
+ developer_permissions + [:admin_environment, :admin_deployment]
end
where(:project_visibility, :access_level, :role, :allowed) do
@@ -2086,15 +2075,73 @@ RSpec.describe ProjectPolicy do
expect_disallowed(*permissions_abilities(role))
end
end
+ end
+ end
- def permissions_abilities(role)
- case role
- when :maintainer
- maintainer_environments_permissions
- when :developer
- developer_environments_permissions
+ describe 'monitor feature' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:guest_permissions) { [] }
+
+ let(:developer_permissions) do
+ guest_permissions + [
+ :read_sentry_issue, :read_alert_management_alert, :metrics_dashboard,
+ :update_sentry_issue, :update_alert_management_alert
+ ]
+ end
+
+ let(:maintainer_permissions) { developer_permissions }
+
+ where(:project_visibility, :access_level, :role, :allowed) do
+ :public | ProjectFeature::ENABLED | :maintainer | true
+ :public | ProjectFeature::ENABLED | :developer | true
+ :public | ProjectFeature::ENABLED | :guest | true
+ :public | ProjectFeature::ENABLED | :anonymous | true
+ :public | ProjectFeature::PRIVATE | :maintainer | true
+ :public | ProjectFeature::PRIVATE | :developer | true
+ :public | ProjectFeature::PRIVATE | :guest | true
+ :public | ProjectFeature::PRIVATE | :anonymous | false
+ :public | ProjectFeature::DISABLED | :maintainer | false
+ :public | ProjectFeature::DISABLED | :developer | false
+ :public | ProjectFeature::DISABLED | :guest | false
+ :public | ProjectFeature::DISABLED | :anonymous | false
+ :internal | ProjectFeature::ENABLED | :maintainer | true
+ :internal | ProjectFeature::ENABLED | :developer | true
+ :internal | ProjectFeature::ENABLED | :guest | true
+ :internal | ProjectFeature::ENABLED | :anonymous | false
+ :internal | ProjectFeature::PRIVATE | :maintainer | true
+ :internal | ProjectFeature::PRIVATE | :developer | true
+ :internal | ProjectFeature::PRIVATE | :guest | true
+ :internal | ProjectFeature::PRIVATE | :anonymous | false
+ :internal | ProjectFeature::DISABLED | :maintainer | false
+ :internal | ProjectFeature::DISABLED | :developer | false
+ :internal | ProjectFeature::DISABLED | :guest | false
+ :internal | ProjectFeature::DISABLED | :anonymous | false
+ :private | ProjectFeature::ENABLED | :maintainer | true
+ :private | ProjectFeature::ENABLED | :developer | true
+ :private | ProjectFeature::ENABLED | :guest | false
+ :private | ProjectFeature::ENABLED | :anonymous | false
+ :private | ProjectFeature::PRIVATE | :maintainer | true
+ :private | ProjectFeature::PRIVATE | :developer | true
+ :private | ProjectFeature::PRIVATE | :guest | false
+ :private | ProjectFeature::PRIVATE | :anonymous | false
+ :private | ProjectFeature::DISABLED | :maintainer | false
+ :private | ProjectFeature::DISABLED | :developer | false
+ :private | ProjectFeature::DISABLED | :guest | false
+ :private | ProjectFeature::DISABLED | :anonymous | false
+ end
+
+ with_them do
+ let(:current_user) { user_subject(role) }
+ let(:project) { project_subject(project_visibility) }
+
+ it 'allows/disallows the abilities based on the monitor feature access level' do
+ project.project_feature.update!(monitor_access_level: access_level)
+
+ if allowed
+ expect_allowed(*permissions_abilities(role))
else
- guest_environments_permissions
+ expect_disallowed(*permissions_abilities(role))
end
end
end
@@ -2682,6 +2729,43 @@ RSpec.describe ProjectPolicy do
end
end
+ describe 'read_milestone' do
+ context 'when project is public' do
+ let(:project) { public_project_in_group }
+
+ context 'and issues and merge requests are private' do
+ before do
+ project.project_feature.update!(
+ issues_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE
+ )
+ end
+
+ context 'when user is an inherited member from the group' do
+ context 'and user is a guest' do
+ let(:current_user) { inherited_guest }
+
+ it { is_expected.to be_allowed(:read_milestone) }
+ end
+
+ context 'and user is a reporter' do
+ let(:current_user) { inherited_reporter }
+
+ it { is_expected.to be_allowed(:read_milestone) }
+ end
+
+ context 'and user is a developer' do
+ let(:current_user) { inherited_developer }
+
+ it { is_expected.to be_allowed(:read_milestone) }
+ end
+ end
+ end
+ end
+ end
+
+ private
+
def project_subject(project_type)
case project_type
when :public
diff --git a/spec/policies/protected_branch_access_policy_spec.rb b/spec/policies/protected_branch_access_policy_spec.rb
new file mode 100644
index 00000000000..68a130d666a
--- /dev/null
+++ b/spec/policies/protected_branch_access_policy_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedBranchAccessPolicy do
+ let(:user) { create(:user) }
+ let(:protected_branch_access) { create(:protected_branch_merge_access_level) }
+ let(:project) { protected_branch_access.protected_branch.project }
+
+ subject { described_class.new(user, protected_branch_access) }
+
+ context 'as maintainers' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'can be read' do
+ is_expected.to be_allowed(:read_protected_branch)
+ end
+ end
+
+ context 'as guests' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'can not be read' do
+ is_expected.to be_disallowed(:read_protected_branch)
+ end
+ end
+end
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
index bb6dbff18a0..d676de14735 100644
--- a/spec/policies/protected_branch_policy_spec.rb
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -10,15 +10,47 @@ RSpec.describe ProtectedBranchPolicy do
subject { described_class.new(user, protected_branch) }
- it 'branches can be updated via project maintainers' do
- project.add_maintainer(user)
+ context 'as maintainers' do
+ before do
+ project.add_maintainer(user)
+ end
- is_expected.to be_allowed(:update_protected_branch)
+ it 'can be read' do
+ is_expected.to be_allowed(:read_protected_branch)
+ end
+
+ it 'can be created' do
+ is_expected.to be_allowed(:create_protected_branch)
+ end
+
+ it 'can be updated' do
+ is_expected.to be_allowed(:update_protected_branch)
+ end
+
+ it 'can be destroyed' do
+ is_expected.to be_allowed(:destroy_protected_branch)
+ end
end
- it "branches can't be updated by guests" do
- project.add_guest(user)
+ context 'as guests' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'can be read' do
+ is_expected.to be_disallowed(:read_protected_branch)
+ end
+
+ it 'can be created' do
+ is_expected.to be_disallowed(:create_protected_branch)
+ end
+
+ it 'can be updated' do
+ is_expected.to be_disallowed(:update_protected_branch)
+ end
- is_expected.to be_disallowed(:update_protected_branch)
+ it 'cannot be destroyed' do
+ is_expected.to be_disallowed(:destroy_protected_branch)
+ end
end
end
diff --git a/spec/policies/terraform/state_policy_spec.rb b/spec/policies/terraform/state_policy_spec.rb
index 82152920997..d75e20a2c66 100644
--- a/spec/policies/terraform/state_policy_spec.rb
+++ b/spec/policies/terraform/state_policy_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Terraform::StatePolicy do
let_it_be(:project) { create(:project) }
- let_it_be(:terraform_state) { create(:terraform_state, project: project)}
+ let_it_be(:terraform_state) { create(:terraform_state, project: project) }
subject { described_class.new(user, terraform_state) }
diff --git a/spec/policies/terraform/state_version_policy_spec.rb b/spec/policies/terraform/state_version_policy_spec.rb
index 6614e073332..4d41dd44455 100644
--- a/spec/policies/terraform/state_version_policy_spec.rb
+++ b/spec/policies/terraform/state_version_policy_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Terraform::StateVersionPolicy do
let_it_be(:project) { create(:project) }
- let_it_be(:terraform_state) { create(:terraform_state, :with_version, project: project)}
+ let_it_be(:terraform_state) { create(:terraform_state, :with_version, project: project) }
subject { described_class.new(user, terraform_state.latest_version) }
diff --git a/spec/presenters/blobs/notebook_presenter_spec.rb b/spec/presenters/blobs/notebook_presenter_spec.rb
index 12f4ed67897..2f05dc98fb9 100644
--- a/spec/presenters/blobs/notebook_presenter_spec.rb
+++ b/spec/presenters/blobs/notebook_presenter_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe Blobs::NotebookPresenter do
include RepoHelpers
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
- let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') }
- let(:user) { project.first_owner }
- let(:git_blob) { blob.__getobj__ }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository }
+ let_it_be(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') }
+ let_it_be(:user) { project.first_owner }
+ let_it_be(:git_blob) { blob.__getobj__ }
subject(:presenter) { described_class.new(blob, current_user: user) }
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index a278d4dad83..4539c3d06f6 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Ci::PipelinePresenter do
context 'for a detached merge request pipeline' do
let(:event_type) { :detached }
- it { is_expected.to eq('Detached merge request pipeline') }
+ it { is_expected.to eq('Merge request pipeline') }
end
context 'for a merged result pipeline' do
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 7349f444fac..755f1ea6078 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -119,18 +119,20 @@ RSpec.describe Clusters::ClusterPresenter do
subject { cluster_presenter.health_data(clusterable_presenter) }
it do
- is_expected.to include('clusters-path': clusterable_presenter.index_path,
- 'dashboard-endpoint': clusterable_presenter.metrics_dashboard_path(cluster),
- 'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
- 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
- 'empty-getting-started-svg-path': match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
- 'empty-loading-svg-path': match_asset_path('/assets/illustrations/monitoring/loading.svg'),
- 'empty-no-data-svg-path': match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
- 'empty-no-data-small-svg-path': match_asset_path('illustrations/chart-empty-state-small.svg'),
- 'empty-unable-to-connect-svg-path': match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
- 'settings-path': '',
- 'project-path': '',
- 'tags-path': '')
+ is_expected.to include(
+ 'clusters-path': clusterable_presenter.index_path,
+ 'dashboard-endpoint': clusterable_presenter.metrics_dashboard_path(cluster),
+ 'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
+ 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'empty-getting-started-svg-path': match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
+ 'empty-loading-svg-path': match_asset_path('/assets/illustrations/monitoring/loading.svg'),
+ 'empty-no-data-svg-path': match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
+ 'empty-no-data-small-svg-path': match_asset_path('illustrations/chart-empty-state-small.svg'),
+ 'empty-unable-to-connect-svg-path': match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
+ 'settings-path': '',
+ 'project-path': '',
+ 'tags-path': ''
+ )
end
end
diff --git a/spec/presenters/deployments/deployment_presenter_spec.rb b/spec/presenters/deployments/deployment_presenter_spec.rb
new file mode 100644
index 00000000000..689451677f4
--- /dev/null
+++ b/spec/presenters/deployments/deployment_presenter_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Deployments::DeploymentPresenter do
+ let(:deployment) { create(:deployment) }
+ let(:presenter) { described_class.new(deployment) }
+
+ describe '#tags' do
+ it do
+ expect(deployment).to receive(:tags).and_return(['test'])
+ expect(presenter.tags).to eq([{ name: 'test', path: 'tags/test' }])
+ end
+ end
+end
diff --git a/spec/presenters/packages/composer/packages_presenter_spec.rb b/spec/presenters/packages/composer/packages_presenter_spec.rb
index 1f638e5b935..ae88acea61d 100644
--- a/spec/presenters/packages/composer/packages_presenter_spec.rb
+++ b/spec/presenters/packages/composer/packages_presenter_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe ::Packages::Composer::PackagesPresenter do
end
describe '#provider' do
- subject { presenter.provider}
+ subject { presenter.provider }
let(:expected_json) do
{
diff --git a/spec/presenters/packages/conan/package_presenter_spec.rb b/spec/presenters/packages/conan/package_presenter_spec.rb
index d35137cd820..9b74d2e637e 100644
--- a/spec/presenters/packages/conan/package_presenter_spec.rb
+++ b/spec/presenters/packages/conan/package_presenter_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
let_it_be(:package) { create(:conan_package) }
let_it_be(:project) { package.project }
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
- let_it_be(:conan_package_reference) { '123456789'}
+ let_it_be(:conan_package_reference) { '123456789' }
let(:params) { { package_scope: :instance } }
let(:presenter) { described_class.new(package, user, project, params) }
diff --git a/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
index b2bcdf8f03d..39682a3311c 100644
--- a/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Packages::Nuget::PackagesMetadataPresenter do
describe '#count' do
subject { presenter.count }
- it {is_expected.to eq 1}
+ it { is_expected.to eq 1 }
end
describe '#items' do
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index df3e4b985ab..7ff19b1b770 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe ProjectPresenter do
expect(release).to be_truthy
expect(presenter.releases_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including("#{project.releases.count}"),
+ label: a_string_including("#{project.releases.count}"),
link: presenter.project_releases_path(project)
)
end
@@ -216,7 +216,7 @@ RSpec.describe ProjectPresenter do
it 'returns storage data' do
expect(presenter.storage_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including('0 Bytes'),
+ label: a_string_including('0 Bytes'),
link: nil
)
end
@@ -270,7 +270,7 @@ RSpec.describe ProjectPresenter do
it 'returns storage data without usage quotas link for non-admin users' do
expect(presenter.storage_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including('0 Bytes'),
+ label: a_string_including('0 Bytes'),
link: nil
)
end
@@ -280,7 +280,7 @@ RSpec.describe ProjectPresenter do
expect(presenter.storage_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including('0 Bytes'),
+ label: a_string_including('0 Bytes'),
link: presenter.project_usage_quotas_path(project)
)
end
@@ -293,7 +293,7 @@ RSpec.describe ProjectPresenter do
expect(release).to be_truthy
expect(presenter.releases_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including("#{project.releases.count}"),
+ label: a_string_including("#{project.releases.count}"),
link: presenter.project_releases_path(project)
)
end
@@ -484,6 +484,12 @@ RSpec.describe ProjectPresenter do
end
describe '#autodevops_anchor_data' do
+ it 'returns nil if builds feature is not available' do
+ allow(project).to receive(:feature_available?).with(:builds, user).and_return(false)
+
+ expect(presenter.autodevops_anchor_data).to be_nil
+ end
+
context 'when Auto Devops is enabled' do
it 'returns anchor data' do
allow(project).to receive(:auto_devops_enabled?).and_return(true)
@@ -566,7 +572,7 @@ RSpec.describe ProjectPresenter do
it 'returns upload_anchor_data' do
expect(presenter.upload_anchor_data).to have_attributes(
is_link: false,
- label: a_string_including('Upload file'),
+ label: a_string_including('Upload file'),
data: {
"can_push_code" => "true",
"original_branch" => "master",
@@ -613,7 +619,7 @@ RSpec.describe ProjectPresenter do
end
context 'empty repo' do
- let(:project) { create(:project, :stubbed_repository)}
+ let(:project) { create(:project, :stubbed_repository) }
context 'for a guest user' do
it 'orders the items correctly' do
diff --git a/spec/requests/admin/hook_logs_controller_spec.rb b/spec/requests/admin/hook_logs_controller_spec.rb
new file mode 100644
index 00000000000..f8d3381c052
--- /dev/null
+++ b/spec/requests/admin/hook_logs_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::HookLogsController, :enable_admin_mode do
+ let_it_be(:user) { create(:admin) }
+ let_it_be_with_refind(:web_hook) { create(:system_hook) }
+ let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
+
+ it_behaves_like WebHooks::HookLogActions do
+ let!(:show_path) { admin_hook_hook_log_path(web_hook, web_hook_log) }
+ let!(:retry_path) { retry_admin_hook_hook_log_path(web_hook, web_hook_log) }
+ let(:edit_hook_path) { edit_admin_hook_path(web_hook) }
+ end
+end
diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb
new file mode 100644
index 00000000000..c99b21c0c27
--- /dev/null
+++ b/spec/requests/api/admin/batched_background_migrations_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Admin::BatchedBackgroundMigrations do
+ let(:admin) { create(:admin) }
+ let(:unauthorized_user) { create(:user) }
+
+ describe 'GET /admin/batched_background_migrations/:id' do
+ let!(:migration) { create(:batched_background_migration, :paused) }
+ let(:database) { :main }
+
+ subject(:show_migration) do
+ get api("/admin/batched_background_migrations/#{migration.id}", admin), params: { database: database }
+ end
+
+ it 'fetches the batched background migration' do
+ show_migration
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(migration.id)
+ expect(json_response['status']).to eq('paused')
+ expect(json_response['job_class_name']).to eq(migration.job_class_name)
+ expect(json_response['progress']).to be_zero
+ end
+ end
+
+ context 'when the batched background migration does not exist' do
+ let(:params) { { database: database } }
+
+ it 'returns 404' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:ci_model) { Ci::ApplicationRecord }
+ let(:database) { :ci }
+
+ it 'uses the correct connection' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ show_migration
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ get api("/admin/batched_background_migrations/#{migration.id}", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'GET /admin/batched_background_migrations' do
+ let!(:migration) { create(:batched_background_migration) }
+
+ context 'when is an admin user' do
+ it 'returns batched background migrations' do
+ get api('/admin/batched_background_migrations', admin)
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['id']).to eq(migration.id)
+ expect(json_response.first['job_class_name']).to eq(migration.job_class_name)
+ expect(json_response.first['table_name']).to eq(migration.table_name)
+ expect(json_response.first['status']).to eq(migration.status_name.to_s)
+ expect(json_response.first['progress']).to be_zero
+ end
+ end
+
+ context 'when multiple database is enabled', :add_ci_connection do
+ let(:database) { :ci }
+ let(:schema) { :gitlab_ci }
+ let(:ci_model) { Ci::ApplicationRecord }
+
+ context 'when CI database is provided' do
+ let(:db_config) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, name: 'fake_db') }
+ let(:default_model) { ActiveRecord::Base }
+ let(:base_models) { { 'fake_db' => default_model, 'ci' => ci_model }.with_indifferent_access }
+
+ it "uses CI database connection" do
+ allow(Gitlab::Database).to receive(:db_config_for_connection).and_return(db_config)
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
+
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ get api('/admin/batched_background_migrations', admin), params: { database: :ci }
+ end
+
+ it 'returns CI database records' do
+ # If we only have one DB we'll see both migrations
+ skip_if_multiple_databases_not_setup
+
+ ci_database_migration = Gitlab::Database::SharedModel.using_connection(ci_model.connection) do
+ create(:batched_background_migration, :active, gitlab_schema: schema)
+ end
+
+ get api('/admin/batched_background_migrations', admin), params: { database: :ci }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['id']).to eq(ci_database_migration.id)
+ expect(json_response.first['job_class_name']).to eq(ci_database_migration.job_class_name)
+ expect(json_response.first['table_name']).to eq(ci_database_migration.table_name)
+ expect(json_response.first['status']).to eq(ci_database_migration.status_name.to_s)
+ expect(json_response.first['progress']).to be_zero
+ end
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ get api('/admin/batched_background_migrations', unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'PUT /admin/batched_background_migrations/:id/resume' do
+ let!(:migration) { create(:batched_background_migration, :paused) }
+ let(:database) { :main }
+
+ subject(:resume) do
+ put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: { database: database }
+ end
+
+ it 'pauses the batched background migration' do
+ resume
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(migration.id)
+ expect(json_response['status']).to eq('active')
+ end
+ end
+
+ context 'when the batched background migration does not exist' do
+ let(:params) { { database: database } }
+
+ it 'returns 404' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ let(:ci_model) { Ci::ApplicationRecord }
+ let(:database) { :ci }
+
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'uses the correct connection' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ resume
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'PUT /admin/batched_background_migrations/:id/pause' do
+ let!(:migration) { create(:batched_background_migration, :active) }
+
+ it 'pauses the batched background migration' do
+ put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: { database: :main }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(migration.id)
+ expect(json_response['status']).to eq('paused')
+ end
+ end
+
+ context 'when the batched background migration does not exist' do
+ let(:params) { { database: :main } }
+
+ it 'returns 404' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when multiple database is enabled' do
+ let(:ci_model) { Ci::ApplicationRecord }
+
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'uses the correct connection' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
+
+ put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: { database: :ci }
+ end
+ end
+
+ context 'when authenticated as a non-admin user' do
+ it 'returns 403' do
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index cc696d76a02..f7539e13b80 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -211,6 +211,68 @@ RSpec.describe API::Branches do
end
it_behaves_like 'repository branches'
+
+ context 'caching' do
+ it 'caches the query' do
+ get api(route), params: { per_page: 1 }
+
+ expect(API::Entities::Branch).not_to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+
+ context 'when increase_branch_cache_expiry is enabled' do
+ it 'uses the cache up to 60 minutes' do
+ time_of_request = Time.current
+
+ get api(route), params: { per_page: 1 }
+
+ travel_to time_of_request + 59.minutes do
+ expect(API::Entities::Branch).not_to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+
+ it 'requests for new value after 60 minutes' do
+ get api(route), params: { per_page: 1 }
+
+ travel_to 61.minutes.from_now do
+ expect(API::Entities::Branch).to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+ end
+
+ context 'when increase_branch_cache_expiry is disabled' do
+ before do
+ stub_feature_flags(increase_branch_cache_expiry: false)
+ end
+
+ it 'uses the cache up to 10 minutes' do
+ time_of_request = Time.current
+
+ get api(route), params: { per_page: 1 }
+
+ travel_to time_of_request + 9.minutes do
+ expect(API::Entities::Branch).not_to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+
+ it 'requests for new value after 10 minutes' do
+ get api(route), params: { per_page: 1 }
+
+ travel_to 11.minutes.from_now do
+ expect(API::Entities::Branch).to receive(:represent)
+
+ get api(route), params: { per_page: 1 }
+ end
+ end
+ end
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -586,13 +648,36 @@ RSpec.describe API::Branches do
let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/unprotect" }
shared_examples_for 'repository unprotected branch' do
- it 'unprotects a single branch' do
- put api(route, current_user)
+ context 'when branch is protected' do
+ let!(:protected_branch) { create(:protected_branch, project: project, name: protected_branch_name) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/branch')
- expect(json_response['name']).to eq(CGI.unescape(branch_name))
- expect(json_response['protected']).to eq(false)
+ it 'unprotects a single branch' do
+ expect_next_instance_of(::ProtectedBranches::DestroyService, project, current_user) do |instance|
+ expect(instance).to receive(:execute).with(protected_branch).and_call_original
+ end
+
+ put api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/branch')
+ expect(json_response['name']).to eq(CGI.unescape(branch_name))
+ expect(json_response['protected']).to eq(false)
+
+ expect { protected_branch.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when branch is not protected' do
+ it 'returns a single branch response' do
+ expect(::ProtectedBranches::DestroyService).not_to receive(:new)
+
+ put api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/branch')
+ expect(json_response['name']).to eq(CGI.unescape(branch_name))
+ expect(json_response['protected']).to eq(false)
+ end
end
context 'when branch does not exist' do
@@ -637,40 +722,40 @@ RSpec.describe API::Branches do
context 'when authenticated', 'as a maintainer' do
let(:current_user) { user }
+ let(:protected_branch_name) { branch_name }
- context "when a protected branch doesn't already exist" do
- it_behaves_like 'repository unprotected branch'
+ it_behaves_like 'repository unprotected branch'
- context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot }
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot }
- it_behaves_like 'repository unprotected branch'
- end
+ it_behaves_like 'repository unprotected branch'
+ end
- context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash }
+ context 'when branch contains a slash' do
+ let(:branch_name) { branch_with_slash }
- it_behaves_like '404 response' do
- let(:request) { put api(route, current_user) }
- end
+ it_behaves_like '404 response' do
+ let(:request) { put api(route, current_user) }
end
+ end
- context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash) }
+ context 'when branch contains an escaped slash' do
+ let(:branch_name) { CGI.escape(branch_with_slash) }
+ let(:protected_branch_name) { branch_with_slash }
- it_behaves_like 'repository unprotected branch'
- end
+ it_behaves_like 'repository unprotected branch'
+ end
- context 'requesting with the escaped project full path' do
- let(:project_id) { CGI.escape(project.full_path) }
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
- it_behaves_like 'repository unprotected branch'
+ it_behaves_like 'repository unprotected branch'
- context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot }
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot }
- it_behaves_like 'repository unprotected branch'
- end
+ it_behaves_like 'repository unprotected branch'
end
end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 2fa1ffb4974..0fb11bf98d2 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -24,8 +24,7 @@ RSpec.describe API::Ci::JobArtifacts do
let(:guest) { create(:project_member, :guest, project: project).user }
let!(:job) do
- create(:ci_build, :success, :tags, pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(:ci_build, :success, :tags, pipeline: pipeline, artifacts_expire_at: 1.day.since)
end
before do
@@ -535,8 +534,7 @@ RSpec.describe API::Ci::JobArtifacts do
context 'with regular branch' do
before do
pipeline.reload
- pipeline.update!(ref: 'master',
- sha: project.commit('master').sha)
+ pipeline.update!(ref: 'master', sha: project.commit('master').sha)
get_for_ref('master')
end
@@ -579,8 +577,7 @@ RSpec.describe API::Ci::JobArtifacts do
stub_artifacts_object_storage
job.success
- project.update!(visibility_level: visibility_level,
- public_builds: public_builds)
+ project.update!(visibility_level: visibility_level, public_builds: public_builds)
get_artifact_file(artifact)
end
@@ -676,8 +673,7 @@ RSpec.describe API::Ci::JobArtifacts do
context 'with branch name containing slash' do
before do
pipeline.reload
- pipeline.update!(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
+ pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
end
it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 57828e50320..b8983e9632e 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -32,8 +32,7 @@ RSpec.describe API::Ci::Jobs do
end
let!(:job) do
- create(:ci_build, :success, :tags, pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(:ci_build, :success, :tags, pipeline: pipeline, artifacts_expire_at: 1.day.since)
end
before do
@@ -94,9 +93,13 @@ RSpec.describe API::Ci::Jobs do
let(:params_with_token) { {} }
end
+ def perform_request
+ get api('/job'), headers: headers_with_token, params: params_with_token
+ end
+
before do |example|
unless example.metadata[:skip_before_request]
- get api('/job'), headers: headers_with_token, params: params_with_token
+ perform_request
end
end
@@ -125,6 +128,15 @@ RSpec.describe API::Ci::Jobs do
expect(json_response['finished_at']).to be_nil
end
+ it 'avoids N+1 queries', :skip_before_request do
+ control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
+
+ running_job = create(:ci_build, :running, project: project, user: user, pipeline: pipeline, artifacts_expire_at: 1.day.since)
+ running_job.save!
+
+ expect { perform_request }.not_to exceed_query_limit(control_count)
+ end
+
it_behaves_like 'returns common pipeline data' do
let(:jobx) { running_job }
end
@@ -237,6 +249,10 @@ RSpec.describe API::Ci::Jobs do
it 'includes environment slug' do
expect(json_response.dig('environment', 'slug')).to eq('production')
end
+
+ it 'includes environment tier' do
+ expect(json_response.dig('environment', 'tier')).to eq('production')
+ end
end
context 'when non-deployment environment action' do
@@ -248,6 +264,10 @@ RSpec.describe API::Ci::Jobs do
it 'includes environment slug' do
expect(json_response.dig('environment', 'slug')).to eq('review')
end
+
+ it 'includes environment tier' do
+ expect(json_response.dig('environment', 'tier')).to eq('development')
+ end
end
context 'when passing the token as params' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index cd58251cfcc..b33b97f90d7 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -17,11 +17,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
describe '/api/v4/jobs' do
- let(:group) { create(:group, :nested) }
+ let_it_be(:group) { create(:group, :nested) }
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
- let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:job) do
create(:ci_build, :pending, :queued, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
@@ -145,7 +146,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:expected_job_info) do
{ 'id' => job.id,
'name' => job.name,
- 'stage' => job.stage,
+ 'stage' => job.stage_name,
'project_id' => job.project.id,
'project_name' => job.project.name }
end
@@ -354,6 +355,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
+ let(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+
before do
project.update!(ci_default_git_depth: nil)
end
@@ -411,7 +415,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when job is made for merge request' do
let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
- let(:merge_request) { create(:merge_request) }
+
+ let_it_be(:merge_request) { create(:merge_request) }
it 'sets branch as ref_type' do
request_job
@@ -546,9 +551,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy',
- stage: 'deploy', stage_idx: 1,
- options: { script: ['bash'], dependencies: [job2.name] })
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline,
+ name: 'deploy',
+ stage: 'deploy',
+ stage_idx: 1,
+ options: { script: ['bash'], dependencies: [job2.name] })
end
before do
@@ -570,9 +578,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'empty_dependencies_job',
- stage: 'deploy', stage_idx: 1,
- options: { script: ['bash'], dependencies: [] })
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline,
+ name: 'empty_dependencies_job',
+ stage: 'deploy',
+ stage_idx: 1,
+ options: { script: ['bash'], dependencies: [] })
end
before do
@@ -722,7 +733,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
describe 'timeout support' do
context 'when project specifies job timeout' do
- let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
+ let_it_be(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
+
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
it 'contains info about timeout taken from project' do
request_job
@@ -827,22 +840,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] }
)
end
-
- context 'when the FF ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it 'returns the image without pull policy' do
- request_job
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to include(
- 'id' => job.id,
- 'image' => { 'name' => 'ruby', 'entrypoint' => nil, 'ports' => [] }
- )
- end
- end
end
context 'when service has pull_policy' do
@@ -867,31 +864,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }]
)
end
-
- context 'when the FF ci_docker_image_pull_policy is disabled' do
- before do
- stub_feature_flags(ci_docker_image_pull_policy: false)
- end
-
- it 'returns the service without pull policy' do
- request_job
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to include(
- 'id' => job.id,
- 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9',
- 'ports' => [], 'variables' => [] }]
- )
- end
- end
end
describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do
let(:job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'test',
- stage: 'deploy', stage_idx: 1,
- options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline,
+ name: 'test',
+ stage: 'deploy',
+ stage_idx: 1,
+ options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
end
context 'when a runner supports this feature' do
@@ -950,8 +933,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when the runner is of group type' do
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, groups: [group]) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:runner) { create(:ci_runner, :group, groups: [group]) }
it_behaves_like 'storing arguments in the application context for the API' do
let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{runner.id}" } }
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 31b85a0b1d6..fa1f713e757 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -889,6 +889,44 @@ RSpec.describe API::Ci::Runners do
end
end
+ it 'avoids N+1 DB queries' do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+ end
+
+ create(:ci_build, :failed, runner: shared_runner, project: project)
+
+ expect do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+ end.not_to exceed_query_limit(control.count)
+ end
+
+ it 'batches loading of commits' do
+ shared_runner = create(:ci_runner, :instance, description: 'Shared runner')
+
+ project_with_repo = create(:project, :repository)
+
+ pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b')
+ create(:ci_build, :running, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+ pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478')
+ create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+ pipeline = create(:ci_pipeline, project: project_with_repo, sha: '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+ expect_next_instance_of(Repository) do |repo|
+ expect(repo).to receive(:commits_by).with(oids: %w[
+ 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863
+ c1c67abbaf91f624347bb3ae96eabe3a1b742478
+ ]).once.and_call_original
+ end
+
+ get api("/runners/#{shared_runner.id}/jobs", admin), params: { per_page: 2, order_by: 'id', sort: 'desc' }
+ end
+
context "when runner doesn't exist" do
it 'returns 404' do
get api('/runners/0/jobs', admin)
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 39be28d7427..dc5d9620dc4 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -478,6 +478,26 @@ RSpec.describe API::CommitStatuses do
.to include 'has already been taken'
end
end
+
+ context 'with partitions' do
+ let(:current_partition_id) { 123 }
+
+ before do
+ allow(Ci::Pipeline)
+ .to receive(:current_partition_value) { current_partition_id }
+ end
+
+ it 'creates records in the current partition' do
+ expect { post api(post_url, developer), params: { state: 'running' } }
+ .to change(CommitStatus, :count).by(1)
+ .and change(Ci::Pipeline, :count).by(1)
+
+ status = CommitStatus.find(json_response['id'])
+
+ expect(status.partition_id).to eq(current_partition_id)
+ expect(status.pipeline.partition_id).to eq(current_partition_id)
+ end
+ end
end
context 'reporter user' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 68fe45cd026..8a08d5203fd 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -7,14 +7,17 @@ RSpec.describe API::Commits do
include ProjectForksHelper
include SessionHelpers
- let(:user) { create(:user) }
- let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
- let(:developer) { create(:user).tap { |u| project.add_developer(u) } }
- let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
let(:project_id) { project.id }
let(:current_user) { nil }
+ let(:group) { create(:group, :public) }
+ let(:inherited_guest) { create(:user).tap { |u| group.add_guest(u) } }
before do
project.add_maintainer(user)
@@ -44,7 +47,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'project commits'
end
@@ -56,311 +59,340 @@ RSpec.describe API::Commits do
end
end
- context 'when authenticated', 'as a maintainer' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'when user is a direct project member' do
+ context 'and user is a maintainer' do
+ let(:current_user) { user }
- it_behaves_like 'project commits'
+ it_behaves_like 'project commits'
- context "since optional parameter" do
- it "returns project commits since provided parameter" do
- commits = project.repository.commits("master", limit: 2)
- after = commits.second.created_at
+ context "since optional parameter" do
+ it "returns project commits since provided parameter" do
+ commits = project.repository.commits("master", limit: 2)
+ after = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
- expect(json_response.size).to eq 2
- expect(json_response.first["id"]).to eq(commits.first.id)
- expect(json_response.second["id"]).to eq(commits.second.id)
- end
+ expect(json_response.size).to eq 2
+ expect(json_response.first["id"]).to eq(commits.first.id)
+ expect(json_response.second["id"]).to eq(commits.second.id)
+ end
- it 'include correct pagination headers' do
- commits = project.repository.commits("master", limit: 2)
- after = commits.second.created_at
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master", limit: 2)
+ after = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
- expect(response).to include_limited_pagination_headers
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context "until optional parameter" do
- it "returns project commits until provided parameter" do
- commits = project.repository.commits("master", limit: 20)
- before = commits.second.created_at
+ context "until optional parameter" do
+ it "returns project commits until provided parameter" do
+ commits = project.repository.commits("master", limit: 20)
+ before = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- if commits.size == 20
- expect(json_response.size).to eq(20)
- else
- expect(json_response.size).to eq(commits.size - 1)
- end
+ if commits.size == 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
- expect(json_response.first["id"]).to eq(commits.second.id)
- expect(json_response.second["id"]).to eq(commits.third.id)
- end
+ expect(json_response.first["id"]).to eq(commits.second.id)
+ expect(json_response.second["id"]).to eq(commits.third.id)
+ end
- it 'include correct pagination headers' do
- commits = project.repository.commits("master", limit: 2)
- before = commits.second.created_at
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master", limit: 2)
+ before = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- expect(response).to include_limited_pagination_headers
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context "invalid xmlschema date parameters" do
- it "returns an invalid parameter error message" do
- get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
+ context "invalid xmlschema date parameters" do
+ it "returns an invalid parameter error message" do
+ get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('since is invalid')
- end
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('since is invalid')
+ end
+ end
- context "with empty ref_name parameter" do
- let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" }
+ context "with empty ref_name parameter" do
+ let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" }
- it_behaves_like 'project commits'
- end
+ it_behaves_like 'project commits'
+ end
- context 'when repository does not exist' do
- let(:project) { create(:project, creator: user, path: 'my.project') }
+ context 'when repository does not exist' do
+ let(:project) { create(:project, creator: user, path: 'my.project') }
- it_behaves_like '404 response' do
- let(:request) { get api(route, current_user) }
- let(:message) { '404 Repository Not Found' }
- end
- end
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Repository Not Found' }
+ end
+ end
- context "path optional parameter" do
- it "returns project commits matching provided path parameter" do
- path = 'files/ruby/popen.rb'
+ context "path optional parameter" do
+ it "returns project commits matching provided path parameter" do
+ path = 'files/ruby/popen.rb'
- get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
- expect(json_response.size).to eq(3)
- expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
- expect(response).to include_limited_pagination_headers
- end
+ expect(json_response.size).to eq(3)
+ expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ expect(response).to include_limited_pagination_headers
+ end
- it 'include correct pagination headers' do
- path = 'files/ruby/popen.rb'
+ it 'include correct pagination headers' do
+ path = 'files/ruby/popen.rb'
- get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
- expect(response).to include_limited_pagination_headers
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context 'all optional parameter' do
- it 'returns all project commits' do
- expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id)
+ context 'all optional parameter' do
+ it 'returns all project commits' do
+ expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id)
- get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user)
+ get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user)
- commit_ids = json_response.map { |c| c['id'] }
+ commit_ids = json_response.map { |c| c['id'] }
- expect(response).to include_limited_pagination_headers
- expect(commit_ids).to eq(expected_commit_ids)
- expect(response.headers['X-Page']).to eql('1')
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(commit_ids).to eq(expected_commit_ids)
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
- context 'first_parent optional parameter' do
- it 'returns all first_parent commits' do
- expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id)
+ context 'first_parent optional parameter' do
+ it 'returns all first_parent commits' do
+ expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id)
- get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
+ get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
- commit_ids = json_response.map { |c| c['id'] }
+ commit_ids = json_response.map { |c| c['id'] }
- expect(response).to include_limited_pagination_headers
- expect(expected_commit_ids.size).to eq(12)
- expect(commit_ids).to eq(expected_commit_ids)
- end
- end
+ expect(response).to include_limited_pagination_headers
+ expect(expected_commit_ids.size).to eq(12)
+ expect(commit_ids).to eq(expected_commit_ids)
+ end
+ end
- context 'with_stats optional parameter' do
- let(:project) { create(:project, :public, :repository) }
+ context 'with_stats optional parameter' do
+ let(:project) { create(:project, :public, :repository) }
- it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do
- let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" }
+ it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do
+ let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" }
- it 'include commits details' do
- commit = project.repository.commit
- get api(route, current_user)
+ it 'include commits details' do
+ commit = project.repository.commit
+ get api(route, current_user)
- expect(json_response.first['stats']['additions']).to eq(commit.stats.additions)
- expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions)
- expect(json_response.first['stats']['total']).to eq(commit.stats.total)
+ expect(json_response.first['stats']['additions']).to eq(commit.stats.additions)
+ expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions)
+ expect(json_response.first['stats']['total']).to eq(commit.stats.total)
+ end
+ end
end
- end
- end
- context 'with pagination params' do
- let(:page) { 1 }
- let(:per_page) { 5 }
- let(:ref_name) { 'master' }
- let(:request) do
- get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
- end
+ context 'with pagination params' do
+ let(:page) { 1 }
+ let(:per_page) { 5 }
+ let(:ref_name) { 'master' }
+ let(:request) do
+ get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+ end
- it 'returns correct headers' do
- request
+ it 'returns correct headers' do
+ request
- expect(response).to include_limited_pagination_headers
- expect(response.headers['Link']).to match(/page=1&per_page=5/)
- expect(response.headers['Link']).to match(/page=2&per_page=5/)
- end
+ expect(response).to include_limited_pagination_headers
+ expect(response.headers['Link']).to match(/page=1&per_page=5/)
+ expect(response.headers['Link']).to match(/page=2&per_page=5/)
+ end
- context 'viewing the first page' do
- it 'returns the first 5 commits' do
- request
+ context 'viewing the first page' do
+ it 'returns the first 5 commits' do
+ request
- commit = project.repository.commit
+ commit = project.repository.commit
- expect(json_response.size).to eq(per_page)
- expect(json_response.first['id']).to eq(commit.id)
- expect(response.headers['X-Page']).to eq('1')
- end
- end
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('1')
+ end
+ end
- context 'viewing the third page' do
- let(:page) { 3 }
+ context 'viewing the third page' do
+ let(:page) { 3 }
- it 'returns the third 5 commits' do
- request
+ it 'returns the third 5 commits' do
+ request
- commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first
+ commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first
- expect(json_response.size).to eq(per_page)
- expect(json_response.first['id']).to eq(commit.id)
- expect(response.headers['X-Page']).to eq('3')
- end
- end
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('3')
+ end
+ end
- context 'when pagination params are invalid' do
- let_it_be(:project) { create(:project, :repository) }
+ context 'when pagination params are invalid' do
+ let_it_be(:project) { create(:project, :repository) }
- using RSpec::Parameterized::TableSyntax
+ using RSpec::Parameterized::TableSyntax
- where(:page, :per_page, :error_message) do
- 0 | nil | 'page does not have a valid value'
- -1 | nil | 'page does not have a valid value'
- 'a' | nil | 'page is invalid'
- nil | 0 | 'per_page does not have a valid value'
- nil | -1 | 'per_page does not have a valid value'
- nil | 'a' | 'per_page is invalid'
- end
+ where(:page, :per_page, :error_message) do
+ 0 | nil | 'page does not have a valid value'
+ -1 | nil | 'page does not have a valid value'
+ 'a' | nil | 'page is invalid'
+ nil | 0 | 'per_page does not have a valid value'
+ nil | -1 | 'per_page does not have a valid value'
+ nil | 'a' | 'per_page is invalid'
+ end
- with_them do
- it 'returns 400 response' do
- request
+ with_them do
+ it 'returns 400 response' do
+ request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(error_message)
- end
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(error_message)
+ end
+ end
- context 'when FF is off' do
- before do
- stub_feature_flags(only_positive_pagination_values: false)
- end
+ context 'when FF is off' do
+ before do
+ stub_feature_flags(only_positive_pagination_values: false)
+ end
- where(:page, :per_page, :error_message, :status) do
- 0 | nil | nil | :success
- -10 | nil | nil | :internal_server_error
- 'a' | nil | 'page is invalid' | :bad_request
- nil | 0 | 'per_page has a value not allowed' | :bad_request
- nil | -1 | nil | :success
- nil | 'a' | 'per_page is invalid' | :bad_request
- end
+ where(:page, :per_page, :error_message, :status) do
+ 0 | nil | nil | :success
+ -10 | nil | nil | :internal_server_error
+ 'a' | nil | 'page is invalid' | :bad_request
+ nil | 0 | 'per_page has a value not allowed' | :bad_request
+ nil | -1 | nil | :success
+ nil | 'a' | 'per_page is invalid' | :bad_request
+ end
- with_them do
- it 'returns a response' do
- request
+ with_them do
+ it 'returns a response' do
+ request
- expect(response).to have_gitlab_http_status(status)
+ expect(response).to have_gitlab_http_status(status)
- if error_message
- expect(json_response['error']).to eq(error_message)
+ if error_message
+ expect(json_response['error']).to eq(error_message)
+ end
+ end
end
end
end
end
- end
- end
- context 'with order parameter' do
- let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" }
+ context 'with order parameter' do
+ let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" }
- context 'set to topo' do
- let(:order) { 'topo' }
+ context 'set to topo' do
+ let(:order) { 'topo' }
- # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876
- # * 0031876
- # |\
- # | * 48ca272
- # | * 335bc94
- # * | bf6e164
- # * | 9d526f8
- # |/
- # * 1039376
- it 'returns project commits ordered by topo order' do
- commits = project.repository.commits("0031876", limit: 6, order: 'topo')
+ # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876
+ # * 0031876
+ # |\
+ # | * 48ca272
+ # | * 335bc94
+ # * | bf6e164
+ # * | 9d526f8
+ # |/
+ # * 1039376
+ it 'returns project commits ordered by topo order' do
+ commits = project.repository.commits("0031876", limit: 6, order: 'topo')
- get api(route, current_user)
+ get api(route, current_user)
- expect(json_response.size).to eq(6)
- expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
- end
- end
+ expect(json_response.size).to eq(6)
+ expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
+ end
+ end
+
+ context 'set to default' do
+ let(:order) { 'default' }
+
+ # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876
+ # * 0031876
+ # |\
+ # * | bf6e164
+ # | * 48ca272
+ # * | 9d526f8
+ # | * 335bc94
+ # |/
+ # * 1039376
+ it 'returns project commits ordered by default order' do
+ commits = project.repository.commits("0031876", limit: 6, order: 'default')
+
+ get api(route, current_user)
+
+ expect(json_response.size).to eq(6)
+ expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
+ end
+ end
- context 'set to default' do
- let(:order) { 'default' }
+ context 'set to an invalid parameter' do
+ let(:order) { 'invalid' }
- # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876
- # * 0031876
- # |\
- # * | bf6e164
- # | * 48ca272
- # * | 9d526f8
- # | * 335bc94
- # |/
- # * 1039376
- it 'returns project commits ordered by default order' do
- commits = project.repository.commits("0031876", limit: 6, order: 'default')
+ it_behaves_like '400 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+ end
- get api(route, current_user)
+ context 'with the optional trailers parameter' do
+ it 'includes the Git trailers' do
+ get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user)
- expect(json_response.size).to eq(6)
- expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
+ commit = json_response[0]
+
+ expect(commit['trailers']).to eq(
+ 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
+ )
+ end
end
end
+ end
- context 'set to an invalid parameter' do
- let(:order) { 'invalid' }
+ context 'when user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- it_behaves_like '400 response' do
- let(:request) { get api(route, current_user) }
+ context 'and user is a guest' do
+ let(:current_user) { inherited_guest }
+
+ it_behaves_like 'project commits'
end
end
- end
- context 'with the optional trailers parameter' do
- it 'includes the Git trailers' do
- get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user)
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository, group: group) }
- commit = json_response[0]
+ context 'and user is a guest' do
+ let(:current_user) { inherited_guest }
- expect(commit['trailers']).to eq(
- 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
- )
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
end
end
end
@@ -382,6 +414,9 @@ RSpec.describe API::Commits do
end
describe 'create' do
+ let_it_be(:sequencer) { FactoryBot::Sequence.new(:new_file_path) { |n| "files/test/#{n}.rb" } }
+
+ let(:new_file_path) { sequencer.next }
let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' }
let(:invalid_c_params) do
{
@@ -404,7 +439,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
- file_path: 'foo/bar/baz.txt',
+ file_path: new_file_path,
content: 'puts 8'
}
]
@@ -418,7 +453,7 @@ RSpec.describe API::Commits do
actions: [
{
action: 'create',
- file_path: 'foo/bar/baz.txt',
+ file_path: new_file_path,
content: 'puts 🦊'
}
]
@@ -466,11 +501,57 @@ RSpec.describe API::Commits do
end
context 'a new file in project repo' do
- before do
- post api(url, user), params: valid_c_params
+ context 'when user is a direct project member' do
+ before do
+ post api(url, user), params: valid_c_params
+ end
+
+ it_behaves_like 'successfully creates the commit'
end
- it_behaves_like "successfully creates the commit"
+ context 'when user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(url, inherited_guest), params: valid_c_params }
+ let(:message) { '403 Forbidden' }
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(url, inherited_guest), params: valid_c_params }
+ let(:message) { '403 Forbidden' }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when repository is empty' do
+ let!(:project) { create(:project, :empty_repo) }
+
+ context 'when params are valid' do
+ before do
+ post api(url, user), params: valid_c_params
+ end
+
+ it_behaves_like "successfully creates the commit"
+ end
+
+ context 'when branch name is invalid' do
+ before do
+ post api(url, user), params: valid_c_params.merge(branch: 'wrong:name')
+ end
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
end
context 'a new file with utf8 chars in project repo' do
@@ -882,6 +963,7 @@ RSpec.describe API::Commits do
end
describe 'multiple operations' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:message) { 'Multiple actions' }
let(:invalid_mo_params) do
{
@@ -951,17 +1033,11 @@ RSpec.describe API::Commits do
}
end
- it 'are committed as one in project repo' do
+ it 'is committed as one in project repo and includes stats' do
post api(url, user), params: valid_mo_params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq(message)
- end
-
- it 'includes the commit stats' do
- post api(url, user), params: valid_mo_params
-
- expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include 'stats'
end
@@ -1047,7 +1123,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/refs' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
let(:tag) { project.repository.find_tag('v1.1.0') }
let(:commit_id) { tag.dereferenced_target.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" }
@@ -1062,6 +1139,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1151,6 +1230,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1192,8 +1273,14 @@ RSpec.describe API::Commits do
end
shared_examples_for 'ref with unaccessible pipeline' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ status: :created,
+ source: :push,
+ ref: 'master',
+ sha: commit.sha,
+ protected: false)
end
it 'does not include last_pipeline' do
@@ -1231,7 +1318,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref commit'
it_behaves_like 'ref with pipeline'
@@ -1261,6 +1348,7 @@ RSpec.describe API::Commits do
context 'when builds are disabled' do
before do
project
+ .reload
.project_feature
.update!(builds_access_level: ProjectFeature::DISABLED)
end
@@ -1312,7 +1400,7 @@ RSpec.describe API::Commits do
context 'with private builds' do
before do
- project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
+ project.reload.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'ref with pipeline'
@@ -1338,8 +1426,8 @@ RSpec.describe API::Commits do
end
context 'when authenticated', 'as non_member and project is public' do
- let(:current_user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref with pipeline'
@@ -1392,6 +1480,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1401,7 +1491,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref diff'
end
@@ -1491,6 +1581,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1500,7 +1592,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref comments'
end
@@ -1589,6 +1681,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:commit_id) { commit.id }
let(:branch) { 'master' }
@@ -1626,6 +1719,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1635,7 +1730,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: 'master' } }
@@ -1774,6 +1869,7 @@ RSpec.describe API::Commits do
end
describe 'POST :id/repository/commits/:sha/revert' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:commit) { project.commit(commit_id) }
let(:branch) { 'master' }
@@ -1814,7 +1910,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
let(:request) { post api(route), params: { branch: branch } }
@@ -1921,6 +2017,7 @@ RSpec.describe API::Commits do
end
describe 'POST /projects/:id/repository/commits/:sha/comments' do
+ let(:project) { create(:project, :repository, :private) }
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
let(:note) { 'My comment' }
@@ -1941,6 +2038,8 @@ RSpec.describe API::Commits do
end
context 'when repository is disabled' do
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+
include_context 'disabled repository'
it_behaves_like '404 response' do
@@ -1950,7 +2049,7 @@ RSpec.describe API::Commits do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '400 response' do
let(:request) { post api(route), params: { note: 'My comment' } }
@@ -1970,12 +2069,13 @@ RSpec.describe API::Commits do
it_behaves_like 'ref new comment'
it 'returns the inline comment' do
- post api(route, current_user), params: { note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' }
+ path = project.repository.commit.raw_diffs.first.new_path
+ post api(route, current_user), params: { note: 'My comment', path: path, line: 1, line_type: 'new' }
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/commit_note')
expect(json_response['note']).to eq('My comment')
- expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
+ expect(json_response['path']).to eq(path)
expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
@@ -2050,7 +2150,8 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
- let(:project) { create(:project, :repository, :private) }
+ let_it_be(:project) { create(:project, :repository, :private) }
+
let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
let(:commit) { merged_mr.merge_request_diff.commits.last }
@@ -2082,7 +2183,8 @@ RSpec.describe API::Commits do
end
context 'public project' do
- let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let_it_be(:project) { create(:project, :repository, :public, :merge_requests_private) }
+
let(:non_member) { create(:user) }
it 'responds 403 when only members are allowed to read merge requests' do
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
index e4747e0eb99..b343e0cfc97 100644
--- a/spec/requests/api/conan_instance_packages_spec.rb
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -103,8 +103,7 @@ RSpec.describe API::ConanInstancePackages do
context 'file download endpoints' do
include_context 'conan file download endpoints'
- describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/export/:file_name' do
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
headers: headers
@@ -114,8 +113,7 @@ RSpec.describe API::ConanInstancePackages do
it_behaves_like 'project not found by recipe'
end
- describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
headers: headers
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
index 48e36b55a68..4e6af9942ef 100644
--- a/spec/requests/api/conan_project_packages_spec.rb
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -102,8 +102,7 @@ RSpec.describe API::ConanProjectPackages do
context 'file download endpoints', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326194' do
include_context 'conan file download endpoints'
- describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/export/:file_name' do
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
subject do
get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
headers: headers
@@ -113,8 +112,7 @@ RSpec.describe API::ConanProjectPackages do
it_behaves_like 'project not found by project id'
end
- describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
subject do
get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
headers: headers
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index d881d4350fb..9dbb75becf8 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -36,12 +36,42 @@ RSpec.describe API::DebianGroupPackages do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
+ using RSpec::Parameterized::TableSyntax
+
let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{package.debian_distribution.codename}/#{project.id}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
let(:file_name) { params[:file_name] }
- using RSpec::Parameterized::TableSyntax
-
where(:file_name, :success_body) do
'sample_1.2.3~alpha2.tar.xz' | /^.7zXZ/
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
@@ -53,6 +83,12 @@ RSpec.describe API::DebianGroupPackages do
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
+
+ context 'for bumping last downloaded at' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'bumping the package last downloaded at field'
+ end
+ end
end
end
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index bd68bf912e1..6bef669cb3a 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -36,12 +36,42 @@ RSpec.describe API::DebianProjectPackages do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/source/Sources' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/source/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ end
+
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ end
+
describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do
+ using RSpec::Parameterized::TableSyntax
+
let(:url) { "/projects/#{container.id}/packages/debian/pool/#{package.debian_distribution.codename}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
let(:file_name) { params[:file_name] }
- using RSpec::Parameterized::TableSyntax
-
where(:file_name, :success_body) do
'sample_1.2.3~alpha2.tar.xz' | /^.7zXZ/
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
@@ -53,6 +83,12 @@ RSpec.describe API::DebianProjectPackages do
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
+
+ context 'for bumping last downloaded at' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'bumping the package last downloaded at field'
+ end
+ end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 24c3ee59c18..24e0e5d3180 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -14,9 +14,10 @@ RSpec.describe API::Deployments do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:production) { create(:environment, :production, project: project) }
let_it_be(:staging) { create(:environment, :staging, project: project) }
- let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, ref: 'master', created_at: Time.now, updated_at: Time.now) }
- let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
- let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
+ let_it_be(:build) { create(:ci_build, :success, project: project) }
+ let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, deployable: build, ref: 'master', created_at: Time.now, updated_at: Time.now) }
+ let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
+ let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
def perform_request(params = {})
get api("/projects/#{project.id}/deployments", user), params: params
@@ -104,7 +105,7 @@ RSpec.describe API::Deployments do
control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
- create(:deployment, :success, project: project, iid: 21, ref: 'master')
+ create(:deployment, :success, project: project, deployable: build, iid: 21, ref: 'master')
expect { perform_request }.not_to exceed_query_limit(control_count)
end
diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb
index a1aedc1d6b2..bf7eec167f5 100644
--- a/spec/requests/api/feature_flags_spec.rb
+++ b/spec/requests/api/feature_flags_spec.rb
@@ -365,8 +365,8 @@ RSpec.describe API::FeatureFlags do
describe 'PUT /projects/:id/feature_flags/:name' do
context 'with a version 2 feature flag' do
let!(:feature_flag) do
- create(:operations_feature_flag, :new_version_flag, project: project, active: true,
- name: 'feature1', description: 'old description')
+ create(:operations_feature_flag, :new_version_flag,
+ project: project, active: true, name: 'feature1', description: 'old description')
end
it 'returns a 404 if the feature flag does not exist' do
@@ -591,8 +591,8 @@ RSpec.describe API::FeatureFlags do
it 'deletes a feature flag strategy' do
strategy_a = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'userA,userB' })
+ strategy_b = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'userA,userB' })
params = {
strategies: [{
id: strategy_a.id,
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 06d22e7e218..e95a626b4aa 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,13 +5,21 @@ require 'spec_helper'
RSpec.describe API::Files do
include RepoHelpers
- let(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be_with_refind(:user) { create(:user) }
+ let_it_be(:inherited_guest) { create(:user) }
+ let_it_be(:inherited_reporter) { create(:user) }
+ let_it_be(:inherited_developer) { create(:user) }
+
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
- let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
- let(:executable_file_path) { "files%2Fexecutables%2Fls" }
- let(:rouge_file_path) { "%2e%2e%2f" }
- let(:absolute_path) { "%2Fetc%2Fpasswd.rb" }
+ let(:file_path) { 'files%2Fruby%2Fpopen%2Erb' }
+ let(:file_name) { 'popen.rb' }
+ let(:last_commit_id) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
+ let(:content_sha256) { 'c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887' }
+ let(:executable_file_path) { 'files%2Fexecutables%2Fls' }
+ let(:invalid_file_path) { '%2e%2e%2f' }
+ let(:absolute_path) { '%2Fetc%2Fpasswd.rb' }
let(:invalid_file_message) { 'file_path should be a valid file path' }
let(:params) do
{
@@ -46,6 +54,12 @@ RSpec.describe API::Files do
fake_class.new
end
+ before_all do
+ group.add_guest(inherited_guest)
+ group.add_reporter(inherited_reporter)
+ group.add_developer(inherited_developer)
+ end
+
before do
project.add_developer(user)
end
@@ -70,8 +84,10 @@ RSpec.describe API::Files do
expect(helper.headers).to eq({ 'X-Gitlab-Test' => '1' })
end
- it 'raises exception if value is an Enumerable' do
- expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
+ context 'when value is an Enumerable' do
+ it 'raises an exception' do
+ expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
+ end
end
end
@@ -87,12 +103,12 @@ RSpec.describe API::Files do
end
end
- describe "HEAD /projects/:id/repository/files/:file_path" do
+ describe 'HEAD /projects/:id/repository/files/:file_path' do
shared_examples_for 'repository files' do
let(:options) { {} }
it 'returns 400 when file path is invalid' do
- head api(route(rouge_file_path), current_user, **options), params: params
+ head api(route(invalid_file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -106,16 +122,16 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
- expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
- expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(response.headers['X-Gitlab-Content-Sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
+ expect(response.headers['X-Gitlab-File-Name']).to eq(file_name)
+ expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id)
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256)
end
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
head api(route(file_path), current_user, **options), params: params
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
- .to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
+ .to eq(content_sha256)
expect_next_instance_of(Gitlab::Git::Blob) do |instance|
expect(instance).not_to receive(:load_all_data!)
@@ -126,8 +142,8 @@ RSpec.describe API::Files do
it 'returns file by commit sha' do
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
head api(route(file_path), current_user, **options), params: params
@@ -137,15 +153,15 @@ RSpec.describe API::Files do
end
context 'when mandatory params are not given' do
- it "responds with a 400 status" do
- head api(route("any%2Ffile"), current_user, **options)
+ it 'responds with a 400 status' do
+ head api(route('any%2Ffile'), current_user, **options)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when file_path does not exist' do
- it "responds with a 404 status" do
+ it 'responds with a 404 status' do
params[:ref] = 'master'
head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params
@@ -157,7 +173,7 @@ RSpec.describe API::Files do
context 'when file_path does not exist' do
include_context 'disabled repository'
- it "responds with a 403 status" do
+ it 'responds with a 403 status' do
head api(route(file_path), current_user, **options), params: params
expect(response).to have_gitlab_http_status(:forbidden)
@@ -165,20 +181,22 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- end
- context 'when unauthenticated', 'and project is private' do
- it "responds with a 404 status" do
- current_user = nil
+ context 'and project is private' do
+ it 'responds with a 404 status' do
+ current_user = nil
- head api(route(file_path), current_user), params: params
+ head api(route(file_path), current_user), params: params
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
@@ -190,25 +208,41 @@ RSpec.describe API::Files do
end
end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository files' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ end
end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { head api(route(file_path), guest), params: params }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { head api(route(file_path), guest), params: params }
+ end
end
end
end
- describe "GET /projects/:id/repository/files/:file_path" do
- shared_examples_for 'repository files' do
- let(:options) { {} }
+ describe 'GET /projects/:id/repository/files/:file_path' do
+ let(:options) { {} }
+
+ shared_examples 'returns non-executable file attributes as json' do
+ specify do
+ get api(route(file_path), api_user, **options), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ expect(json_response['file_name']).to eq(file_name)
+ expect(json_response['last_commit_id']).to eq(last_commit_id)
+ expect(json_response['content_sha256']).to eq(content_sha256)
+ expect(json_response['execute_filemode']).to eq(false)
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
+ end
+ end
+ shared_examples_for 'repository files' do
it 'returns 400 for invalid file path' do
- get api(route(rouge_file_path), api_user, **options), params: params
+ get api(route(invalid_file_path), api_user, **options), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -218,17 +252,7 @@ RSpec.describe API::Files do
subject { get api(route(absolute_path), api_user, **options), params: params }
end
- it 'returns file attributes as json' do
- get api(route(file_path), api_user, **options), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['file_path']).to eq(CGI.unescape(file_path))
- expect(json_response['file_name']).to eq('popen.rb')
- expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
- expect(json_response['execute_filemode']).to eq(false)
- expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
- end
+ it_behaves_like 'returns non-executable file attributes as json'
context 'for executable file' do
it 'returns file attributes as json' do
@@ -247,7 +271,7 @@ RSpec.describe API::Files do
end
it 'returns json when file has txt extension' do
- file_path = "bar%2Fbranch-test.txt"
+ file_path = 'bar%2Fbranch-test.txt'
get api(route(file_path), api_user, **options), params: params
@@ -277,8 +301,8 @@ RSpec.describe API::Files do
it 'returns file by commit sha' do
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
get api(route(file_path), api_user, **options), params: params
@@ -289,9 +313,9 @@ RSpec.describe API::Files do
end
it 'returns raw file info' do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
expect_to_send_git_blob(api(url, api_user, **options), params)
- expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
end
it 'returns blame file info' do
@@ -303,16 +327,16 @@ RSpec.describe API::Files do
end
it 'sets inline content disposition by default' do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
get api(url, api_user, **options), params: params
- expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb))
+ expect(headers['Content-Disposition']).to eq(%(inline; filename="#{file_name}"; filename*=UTF-8''#{file_name}))
end
context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route("any%2Ffile"), current_user, **options) }
+ let(:request) { get api(route('any%2Ffile'), current_user, **options) }
end
end
@@ -334,40 +358,96 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- let(:api_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ let(:api_user) { nil }
+ end
end
- end
- context 'when PATs are used' do
- it_behaves_like 'repository files' do
- let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
- let(:current_user) { user }
- let(:api_user) { nil }
- let(:options) { { personal_access_token: token } }
+ context 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params: params }
+ let(:message) { '404 Project Not Found' }
+ end
end
end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route(file_path)), params: params }
- let(:message) { '404 Project Not Found' }
- end
- end
+ context 'when authenticated' do
+ context 'and user is a direct project member' do
+ context 'and project is private' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ let(:api_user) { user }
+ end
+
+ context 'and PATs are used' do
+ it_behaves_like 'repository files' do
+ let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
+ let(:current_user) { user }
+ let(:api_user) { nil }
+ let(:options) { { personal_access_token: token } }
+ end
+ end
+ end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository files' do
- let(:current_user) { user }
- let(:api_user) { user }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), guest), params: params }
+ end
+ end
+ end
end
end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route(file_path), guest), params: params }
+ context 'when authenticated' do
+ context 'and user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_guest }
+ end
+ end
+
+ context 'and user is a reporter' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_reporter }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_developer }
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), inherited_guest), params: params }
+ end
+ end
+
+ context 'and user is a reporter' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_reporter }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'returns non-executable file attributes as json' do
+ let(:api_user) { inherited_developer }
+ end
+ end
+ end
end
end
end
@@ -406,11 +486,10 @@ RSpec.describe API::Files do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
- expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
- expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
- expect(response.headers['X-Gitlab-Content-Sha256'])
- .to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
- expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("false")
+ expect(response.headers['X-Gitlab-File-Name']).to eq(file_name)
+ expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id)
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256)
+ expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('false')
end
context 'for executable file' do
@@ -424,13 +503,13 @@ RSpec.describe API::Files do
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f')
expect(response.headers['X-Gitlab-Content-Sha256'])
.to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191')
- expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("true")
+ expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('true')
end
end
end
it 'returns 400 when file path is invalid' do
- get api(route(rouge_file_path) + '/blame', current_user), params: params
+ get api(route(invalid_file_path) + '/blame', current_user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -573,29 +652,33 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository blame files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository blame files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route(file_path)), params: params }
- let(:message) { '404 Project Not Found' }
+ context 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params: params }
+ let(:message) { '404 Project Not Found' }
+ end
end
end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository blame files' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository blame files' do
+ let(:current_user) { user }
+ end
end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route(file_path) + '/blame', guest), params: params }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path) + '/blame', guest), params: params }
+ end
end
end
@@ -614,10 +697,10 @@ RSpec.describe API::Files do
end
end
- describe "GET /projects/:id/repository/files/:file_path/raw" do
+ describe 'GET /projects/:id/repository/files/:file_path/raw' do
shared_examples_for 'repository raw files' do
it 'returns 400 when file path is invalid' do
- get api(route(rouge_file_path) + "/raw", current_user), params: params
+ get api(route(invalid_file_path) + '/raw', current_user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -628,7 +711,7 @@ RSpec.describe API::Files do
end
it 'returns raw file info' do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
expect_to_send_git_blob(api(url, current_user), params)
end
@@ -639,39 +722,39 @@ RSpec.describe API::Files do
end
it 'returns response :ok', :aggregate_failures do
- url = route(file_path) + "/raw"
+ url = route(file_path) + '/raw'
expect_to_send_git_blob(api(url, current_user), {})
end
end
it 'returns raw file info for files with dots' do
- url = route('.gitignore') + "/raw"
+ url = route('.gitignore') + '/raw'
expect_to_send_git_blob(api(url, current_user), params)
end
it 'returns file by commit sha' do
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
- expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params)
+ expect_to_send_git_blob(api(route(file_path) + '/raw', current_user), params)
end
it 'sets no-cache headers' do
- url = route('.gitignore') + "/raw"
+ url = route('.gitignore') + '/raw'
expect_to_send_git_blob(api(url, current_user), params)
- expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
- expect(response.headers["Pragma"]).to eq("no-cache")
- expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
+ expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache')
+ expect(response.headers['Pragma']).to eq('no-cache')
+ expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT')
end
context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route("any%2Ffile"), current_user) }
+ let(:request) { get api(route('any%2Ffile'), current_user) }
end
end
@@ -693,29 +776,33 @@ RSpec.describe API::Files do
end
end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository raw files' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated' do
+ context 'and project is public' do
+ it_behaves_like 'repository raw files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route(file_path)), params: params }
- let(:message) { '404 Project Not Found' }
+ context 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params: params }
+ let(:message) { '404 Project Not Found' }
+ end
end
end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository raw files' do
- let(:current_user) { user }
+ context 'when authenticated' do
+ context 'and user is a developer' do
+ it_behaves_like 'repository raw files' do
+ let(:current_user) { user }
+ end
end
- end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route(file_path), guest), params: params }
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), guest), params: params }
+ end
end
end
@@ -724,139 +811,205 @@ RSpec.describe API::Files do
token = create(:personal_access_token, scopes: ['read_repository'], user: user)
# This file is deleted on HEAD
- file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
- params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- url = api(route(file_path) + "/raw", personal_access_token: token)
+ file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
+ params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
+ url = api(route(file_path) + '/raw', personal_access_token: token)
expect_to_send_git_blob(url, params)
end
end
end
- describe "POST /projects/:id/repository/files/:file_path" do
- let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
+ describe 'POST /projects/:id/repository/files/:file_path' do
+ let!(:file_path) { 'new_subfolder%2Fnewfile%2Erb' }
+
let(:params) do
{
- branch: "master",
- content: "puts 8",
- commit_message: "Added newfile"
+ branch: 'master',
+ content: 'puts 8',
+ commit_message: 'Added newfile'
}
end
let(:executable_params) do
{
- branch: "master",
- content: "puts 8",
- commit_message: "Added newfile",
+ branch: 'master',
+ content: 'puts 8',
+ commit_message: 'Added newfile',
execute_filemode: true
}
end
- it 'returns 400 when file path is invalid' do
- post api(route(rouge_file_path), user), params: params
+ shared_examples 'creates a new file in the project repo' do
+ specify do
+ post api(route(file_path), current_user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(invalid_file_message)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(current_user.email)
+ expect(last_commit.author_name).to eq(current_user.name)
+ expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
+ end
end
- it_behaves_like 'when path is absolute' do
- subject { post api(route(absolute_path), user), params: params }
- end
+ context 'when authenticated', 'as a direct project member' do
+ context 'when project is private' do
+ context 'and user is a developer' do
+ it 'returns 400 when file path is invalid' do
+ post api(route(invalid_file_path), user), params: params
- it "creates a new file in project repo" do
- post api(route(file_path), user), params: params
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(invalid_file_message)
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
- expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
- end
+ it_behaves_like 'when path is absolute' do
+ subject { post api(route(absolute_path), user), params: params }
+ end
- it "creates a new executable file in project repo" do
- post api(route(file_path), user), params: executable_params
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { user }
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
- expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
- end
+ it 'creates a new executable file in project repo' do
+ post api(route(file_path), user), params: executable_params
- it "returns a 400 bad request if no mandatory params given" do
- post api(route("any%2Etxt"), user)
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ context 'when no mandatory params given' do
+ it 'returns a 400 bad request' do
+ post api(route('any%2Etxt'), user)
- it 'returns a 400 bad request if the commit message is empty' do
- params[:commit_message] = ''
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
- post api(route(file_path), user), params: params
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ it 'returns a 400 bad request' do
+ post api(route(file_path), user), params: params
- it "returns a 400 if editor fails to create file" do
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file')
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
- post api(route("any%2Etxt"), user), params: params
+ context 'when editor fails to create file' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file')
+ end
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ it 'returns a 400 bad request' do
+ post api(route('any%2Etxt'), user), params: params
- context 'with PATs' do
- it 'returns 403 with `read_repository` scope' do
- token = create(:personal_access_token, scopes: ['read_repository'], user: user)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
- post api(route(file_path), personal_access_token: token), params: params
+ context 'and PATs are used' do
+ it 'returns 403 with `read_repository` scope' do
+ token = create(:personal_access_token, scopes: ['read_repository'], user: user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ post api(route(file_path), personal_access_token: token), params: params
- it 'returns 201 with `api` scope' do
- token = create(:personal_access_token, scopes: ['api'], user: user)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
- post api(route(file_path), personal_access_token: token), params: params
+ it 'returns 201 with `api` scope' do
+ token = create(:personal_access_token, scopes: ['api'], user: user)
- expect(response).to have_gitlab_http_status(:created)
- end
- end
+ post api(route(file_path), personal_access_token: token), params: params
- context "when specifying an author" do
- it "creates a new file with the specified author" do
- params.merge!(author_email: author_email, author_name: author_name)
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
- post api(route("new_file_with_author%2Etxt"), user), params: params
+ context 'and the repo is empty' do
+ let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
- expect(response).to have_gitlab_http_status(:created)
- expect(response.media_type).to eq('application/json')
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(author_email)
- expect(last_commit.author_name).to eq(author_name)
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { user }
+ let(:file_path) { 'newfile%2Erb' }
+ end
+ end
+
+ context 'when specifying an author' do
+ it 'creates a new file with the specified author' do
+ params.merge!(author_email: author_email, author_name: author_name)
+
+ post api(route('new_file_with_author%2Etxt'), user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response.media_type).to eq('application/json')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
end
end
- context 'when the repo is empty' do
- let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+ context 'when authenticated' do
+ context 'and user is an inherited member from the group' do
+ context 'when project is public with private repository' do
+ let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
- it "creates a new file in project repo" do
- post api(route("newfile%2Erb"), user), params: params
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_guest), params: params }
+ end
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['file_path']).to eq('newfile.rb')
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
+ context 'and user is a reporter' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_reporter), params: params }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { inherited_developer }
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+
+ context 'and user is a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_guest), params: params }
+ end
+ end
+
+ context 'and user is a reporter' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route(file_path), inherited_reporter), params: params }
+ end
+ end
+
+ context 'and user is a developer' do
+ it_behaves_like 'creates a new file in the project repo' do
+ let(:current_user) { inherited_developer }
+ end
+ end
+ end
end
end
end
- describe "PUT /projects/:id/repository/files" do
+ describe 'PUT /projects/:id/repository/files' do
let(:params) do
{
branch: 'master',
@@ -865,7 +1018,7 @@ RSpec.describe API::Files do
}
end
- it "updates existing file in project repo" do
+ it 'updates existing file in project repo' do
put api(route(file_path), user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -875,42 +1028,58 @@ RSpec.describe API::Files do
expect(last_commit.author_name).to eq(user.name)
end
- it 'returns a 400 bad request if the commit message is empty' do
- params[:commit_message] = ''
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- put api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ put api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it "returns a 400 bad request if update existing file with stale last commit id" do
- params_with_stale_id = params.merge(last_commit_id: 'stale')
+ context 'when updating an existing file with stale last commit id' do
+ let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') }
- put api(route(file_path), user), params: params_with_stale_id
+ it 'returns a 400 bad request' do
+ put api(route(file_path), user), params: params_with_stale_id
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.'))
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.'))
+ end
end
- it "updates existing file in project repo with accepts correct last commit id" do
- last_commit = Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- params_with_correct_id = params.merge(last_commit_id: last_commit.id)
+ context 'with correct last commit id' do
+ let(:last_commit) do
+ Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
+ end
- put api(route(file_path), user), params: params_with_correct_id
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
- expect(response).to have_gitlab_http_status(:ok)
+ it 'updates existing file in project repo' do
+ put api(route(file_path), user), params: params_with_correct_id
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- it "returns 400 when file path is invalid" do
- last_commit = Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- params_with_correct_id = params.merge(last_commit_id: last_commit.id)
+ context 'when file path is invalid' do
+ let(:last_commit) do
+ Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
+ end
- put api(route(rouge_file_path), user), params: params_with_correct_id
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(invalid_file_message)
+ it 'returns a 400 bad request' do
+ put api(route(invalid_file_path), user), params: params_with_correct_id
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(invalid_file_message)
+ end
end
it_behaves_like 'when path is absolute' do
@@ -924,15 +1093,17 @@ RSpec.describe API::Files do
subject { put api(route(absolute_path), user), params: params_with_correct_id }
end
- it "returns a 400 bad request if no params given" do
- put api(route(file_path), user)
+ context 'when no params given' do
+ it 'returns a 400 bad request' do
+ put api(route(file_path), user)
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- context "when specifying an author" do
- it "updates a file with the specified author" do
- params.merge!(author_email: author_email, author_name: author_name, content: "New content")
+ context 'when specifying an author' do
+ it 'updates a file with the specified author' do
+ params.merge!(author_email: author_email, author_name: author_name, content: 'New content')
put api(route(file_path), user), params: params
@@ -982,7 +1153,7 @@ RSpec.describe API::Files do
end
end
- describe "DELETE /projects/:id/repository/files" do
+ describe 'DELETE /projects/:id/repository/files' do
let(:params) do
{
branch: 'master',
@@ -991,7 +1162,7 @@ RSpec.describe API::Files do
end
it 'returns 400 when file path is invalid' do
- delete api(route(rouge_file_path), user), params: params
+ delete api(route(invalid_file_path), user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(invalid_file_message)
@@ -1001,38 +1172,48 @@ RSpec.describe API::Files do
subject { delete api(route(absolute_path), user), params: params }
end
- it "deletes existing file in project repo" do
+ it 'deletes existing file in project repo' do
delete api(route(file_path), user), params: params
expect(response).to have_gitlab_http_status(:no_content)
end
- it "returns a 400 bad request if no params given" do
- delete api(route(file_path), user)
+ context 'when no params given' do
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user)
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it 'returns a 400 bad request if the commit message is empty' do
- params[:commit_message] = ''
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- delete api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it "returns a 400 if fails to delete file" do
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
+ context 'when fails to delete file' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
+ end
end
- delete api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- context "when specifying an author" do
- it "removes a file with the specified author" do
+ context 'when specifying an author' do
+ it 'removes a file with the specified author' do
params.merge!(author_email: author_email, author_name: author_name)
delete api(route(file_path), user), params: params
@@ -1042,7 +1223,7 @@ RSpec.describe API::Files do
end
end
- describe "POST /projects/:id/repository/files with binary file" do
+ describe 'POST /projects/:id/repository/files with binary file' do
let(:file_path) { 'test%2Ebin' }
let(:put_params) do
{
@@ -1063,7 +1244,7 @@ RSpec.describe API::Files do
post api(route(file_path), user), params: put_params
end
- it "remains unchanged" do
+ it 'remains unchanged' do
get api(route(file_path), user), params: get_params
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 3a5c6103781..823eafab734 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -572,6 +572,12 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(expected_status)
end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'bumping the package last downloaded at field' do
+ subject { download_file(auth_header) }
+ end
+ end
end
where(:authenticate_with, :expected_status) do
@@ -587,6 +593,12 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(expected_status)
end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'bumping the package last downloaded at field' do
+ subject { download_file(deploy_token_auth_header) }
+ end
+ end
end
end
@@ -608,6 +620,12 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(expected_status)
end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'bumping the package last downloaded at field' do
+ subject { download_file(personal_access_token_header) }
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index 5f8a895b16e..960fda80dd9 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe 'Query.ciConfig' do
{
"name" => "docker",
"size" => 1,
- "jobs" =>
+ "jobs" =>
{
"nodes" => [
{
@@ -206,7 +206,7 @@ RSpec.describe 'Query.ciConfig' do
{
"name" => "deploy_job",
"size" => 1,
- "jobs" =>
+ "jobs" =>
{
"nodes" => [
{
@@ -332,7 +332,7 @@ RSpec.describe 'Query.ciConfig' do
"only" => { "refs" => %w[branches tags] },
"when" => "on_success",
"tags" => [],
- "needs" => { "nodes" => [] } }
+ "needs" => { "nodes" => [] } }
]
}
}
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
new file mode 100644
index 00000000000..2b5a5d0dc93
--- /dev/null
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)' do
+ include GraphqlHelpers
+ include ReactiveCachingHelpers
+
+ let_it_be(:content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ let_it_be(:project) { create(:project, :custom_repo, :public, files: { '.gitlab-ci.yml' => content }) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { Ci::ListConfigVariablesService.new(project, user) }
+ let(:sha) { project.repository.commit.sha }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ ciConfigVariables(sha: "#{sha}") {
+ key
+ value
+ description
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the user has the correct permissions' do
+ before do
+ project.add_maintainer(user)
+ allow(Ci::ListConfigVariablesService)
+ .to receive(:new)
+ .and_return(service)
+ end
+
+ context 'when the cache is not empty' do
+ before do
+ synchronous_reactive_cache(service)
+ end
+
+ it 'returns the CI variables for the config' do
+ expect(service)
+ .to receive(:execute)
+ .with(sha)
+ .and_call_original
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to contain_exactly(
+ {
+ 'key' => 'DB_NAME',
+ 'value' => 'postgres',
+ 'description' => nil
+ },
+ {
+ 'key' => 'ENVIRONMENT_VAR',
+ 'value' => 'env var value',
+ 'description' => 'env var description'
+ }
+ )
+ end
+ end
+
+ context 'when the cache is empty' do
+ it 'returns nothing' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil
+ end
+ end
+ end
+
+ context 'when the user is not authorized' do
+ before do
+ project.add_guest(user)
+ allow(Ci::ListConfigVariablesService)
+ .to receive(:new)
+ .and_return(service)
+ synchronous_reactive_cache(service)
+ end
+
+ it 'returns nothing' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index 5ea6646ec2c..7baf26c7648 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
query {
group(fullPath: "#{group.full_path}") {
ciVariables {
+ limit
nodes {
id
key
@@ -35,11 +36,18 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
end
it "returns the group's CI variables" do
- variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test',
- masked: false, protected: true, raw: true, environment_scope: 'staging')
+ variable = create(:ci_group_variable,
+ group: group,
+ key: 'TEST_VAR',
+ value: 'test',
+ masked: false,
+ protected: true,
+ raw: true,
+ environment_scope: 'staging')
post_graphql(query, current_user: user)
+ expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(200)
expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({
'id' => variable.to_global_id.to_s,
'key' => 'TEST_VAR',
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index c5c88697bf4..cd6b2de98a1 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Query.ciVariables' do
it "returns the instance's CI variables" do
variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test',
- masked: false, protected: true, raw: true)
+ masked: false, protected: true, raw: true)
post_graphql(query, current_user: user)
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 8c4ab13fc35..fa8fb1d54aa 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -335,4 +335,35 @@ RSpec.describe 'Query.project.pipeline' do
end
end
end
+
+ context 'when querying jobs for multiple projects' do
+ let(:query) do
+ %(
+ query {
+ projects {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create_list(:project, 2).each do |project|
+ project.add_developer(user)
+ create(:ci_build, project: project)
+ end
+ end
+
+ it 'returns an error' do
+ post_graphql(query, current_user: user)
+
+ expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 Project\(s\) at a time./]
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index e61f146b24c..d49a4a7e768 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
query {
project(fullPath: "#{project.full_path}") {
ciVariables {
+ limit
nodes {
id
key
@@ -36,10 +37,11 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
it "returns the project's CI variables" do
variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test',
- masked: false, protected: true, raw: true, environment_scope: 'production')
+ masked: false, protected: true, raw: true, environment_scope: 'production')
post_graphql(query, current_user: user)
+ expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(200)
expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({
'id' => variable.to_global_id.to_s,
'key' => 'TEST_VAR',
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index e17a83d8e47..bd90753f9ad 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -9,24 +9,53 @@ RSpec.describe 'Query.runner(id)' do
let_it_be(:group) { create(:group) }
let_it_be(:active_instance_runner) do
- create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago,
- active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom,
- maintenance_note: '**Test maintenance note**')
+ create(:ci_runner, :instance,
+ description: 'Runner 1',
+ contacted_at: 2.hours.ago,
+ active: true,
+ version: 'adfe156',
+ revision: 'a',
+ locked: true,
+ ip_address: '127.0.0.1',
+ maximum_timeout: 600,
+ access_level: 0,
+ tag_list: %w[tag1 tag2],
+ run_untagged: true,
+ executor_type: :custom,
+ maintenance_note: '**Test maintenance note**')
end
let_it_be(:inactive_instance_runner) do
- create(:ci_runner, :instance, description: 'Runner 2', contacted_at: 1.day.ago, active: false,
- version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true)
+ create(:ci_runner, :instance,
+ description: 'Runner 2',
+ contacted_at: 1.day.ago,
+ active: false,
+ version: 'adfe157',
+ revision: 'b',
+ ip_address: '10.10.10.10',
+ access_level: 1,
+ run_untagged: true)
end
let_it_be(:active_group_runner) do
- create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago,
- active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell)
+ create(:ci_runner, :group,
+ groups: [group],
+ description: 'Group runner 1',
+ contacted_at: 2.hours.ago,
+ active: true,
+ version: 'adfe156',
+ revision: 'a',
+ locked: true,
+ ip_address: '127.0.0.1',
+ maximum_timeout: 600,
+ access_level: 0,
+ tag_list: %w[tag1 tag2],
+ run_untagged: true,
+ executor_type: :shell)
end
- let_it_be(:active_project_runner) { create(:ci_runner, :project) }
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
shared_examples 'runner details fetch' do
let(:query) do
@@ -159,8 +188,16 @@ RSpec.describe 'Query.runner(id)' do
with_them do
let(:project_runner) do
- create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked,
- version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true)
+ create(:ci_runner, :project,
+ description: 'Runner 3',
+ contacted_at: 1.day.ago,
+ active: false,
+ locked: is_locked,
+ version: 'adfe157',
+ revision: 'b',
+ ip_address: '10.10.10.10',
+ access_level: 1,
+ run_untagged: true)
end
let(:query) do
@@ -187,7 +224,6 @@ RSpec.describe 'Query.runner(id)' do
end
describe 'ownerProject' do
- let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) }
let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
@@ -301,7 +337,6 @@ RSpec.describe 'Query.runner(id)' do
end
describe 'for multiple runners' do
- let_it_be(:project1) { create(:project, :test_repo) }
let_it_be(:project2) { create(:project, :test_repo) }
let_it_be(:project_runner1) { create(:ci_runner, :project, projects: [project1, project2], description: 'Runner 1') }
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [], description: 'Runner 2') }
@@ -394,6 +429,8 @@ RSpec.describe 'Query.runner(id)' do
'jobs' => nil, # returning jobs not allowed for more than 1 runner (see RunnerJobsResolver)
'projectCount' => nil,
'projects' => nil)
+
+ expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 CiRunner\(s\) at a time./]
end
end
end
@@ -472,8 +509,8 @@ RSpec.describe 'Query.runner(id)' do
<<~QUERY
{
instance_runner1: #{runner_query(active_instance_runner)}
- project_runner1: #{runner_query(active_project_runner)}
group_runner1: #{runner_query(active_group_runner)}
+ project_runner1: #{runner_query(active_project_runner)}
}
QUERY
end
@@ -493,12 +530,13 @@ RSpec.describe 'Query.runner(id)' do
it 'does not execute more queries per runner', :aggregate_failures do
# warm-up license cache and so on:
- post_graphql(double_query, current_user: user)
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(double_query, **args)
- control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) }
+ control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) }
- expect { post_graphql(double_query, current_user: user) }
- .not_to exceed_query_limit(control)
+ expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control)
expect(graphql_data.count).to eq 6
expect(graphql_data).to match(
@@ -528,4 +566,91 @@ RSpec.describe 'Query.runner(id)' do
))
end
end
+
+ describe 'sorting and pagination' do
+ let(:query) do
+ <<~GQL
+ query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
+ runner(id: $id) {
+ #{fields}
+ }
+ }
+ GQL
+ end
+
+ before do
+ post_graphql(query, current_user: user, variables: variables)
+ end
+
+ context 'with project search term' do
+ let_it_be(:project1) { create(:project, description: 'abc') }
+ let_it_be(:project2) { create(:project, description: 'def') }
+ let_it_be(:project_runner) do
+ create(:ci_runner, :project, projects: [project1, project2])
+ end
+
+ let(:variables) { { id: project_runner.to_global_id.to_s, n: n, project_search_term: search_term } }
+
+ let(:fields) do
+ <<~QUERY
+ projects(search: $projectSearchTerm, first: $n, after: $cursor) {
+ count
+ nodes {
+ id
+ }
+ pageInfo {
+ hasPreviousPage
+ startCursor
+ endCursor
+ hasNextPage
+ }
+ }
+ QUERY
+ end
+
+ let(:projects_data) { graphql_data_at('runner', 'projects') }
+
+ context 'set to empty string' do
+ let(:search_term) { '' }
+
+ context 'with n = 1' do
+ let(:n) { 1 }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns paged result' do
+ expect(projects_data).not_to be_nil
+ expect(projects_data['count']).to eq 2
+ expect(projects_data['pageInfo']['hasNextPage']).to eq true
+ end
+ end
+
+ context 'with n = 2' do
+ let(:n) { 2 }
+
+ it 'returns non-paged result' do
+ expect(projects_data).not_to be_nil
+ expect(projects_data['count']).to eq 2
+ expect(projects_data['pageInfo']['hasNextPage']).to eq false
+ end
+ end
+ end
+
+ context 'set to partial match' do
+ let(:search_term) { 'def' }
+
+ context 'with n = 1' do
+ let(:n) { 1 }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns paged result with no additional pages' do
+ expect(projects_data).not_to be_nil
+ expect(projects_data['count']).to eq 1
+ expect(projects_data['pageInfo']['hasNextPage']).to eq false
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 749f6839cb5..3054b866812 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -69,15 +69,6 @@ RSpec.describe 'Query.runners' do
it_behaves_like 'a working graphql query returning expected runner'
end
-
- context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
- let(:runner_type) { 'PROJECT_TYPE' }
- let(:status) { 'NEVER_CONTACTED' }
-
- let!(:expected_runner) { project_runner }
-
- it_behaves_like 'a working graphql query returning expected runner'
- end
end
describe 'pagination' do
@@ -141,8 +132,13 @@ RSpec.describe 'Group.runners' do
describe 'edges' do
let_it_be(:runner) do
- create(:ci_runner, :group, active: false, version: 'def', revision: '456',
- description: 'Project runner', groups: [group], ip_address: '127.0.0.1')
+ create(:ci_runner, :group,
+ active: false,
+ version: 'def',
+ revision: '456',
+ description: 'Project runner',
+ groups: [group],
+ ip_address: '127.0.0.1')
end
let(:query) do
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index 13b7a22e791..5dd5ad117b0 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -35,7 +35,17 @@ RSpec.describe 'getting custom emoji within namespace' do
expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name)
end
- it 'returns nil when unauthorised' do
+ it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do
+ stub_feature_flags(custom_emoji: false)
+
+ post_graphql(custom_emoji_query(group), current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_data['group']).to be_present
+ expect(graphql_data['group']['customEmoji']).to be_nil
+ end
+
+ it 'returns nil group when unauthorised' do
user = create(:user)
post_graphql(custom_emoji_query(group), current_user: user)
diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb
new file mode 100644
index 00000000000..6da00057449
--- /dev/null
+++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb
@@ -0,0 +1,487 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments Deployments query' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+
+ let(:user) { developer }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'when there are deployments in the environment' do
+ let_it_be(:finished_deployment_old) do
+ create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago)
+ end
+
+ let_it_be(:finished_deployment_new) do
+ create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago)
+ end
+
+ let_it_be(:upcoming_deployment_old) do
+ create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago)
+ end
+
+ let_it_be(:upcoming_deployment_new) do
+ create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago)
+ end
+
+ let_it_be(:other_environment) { create(:environment, project: project) }
+ let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ id
+ iid
+ ref
+ tag
+ sha
+ createdAt
+ updatedAt
+ finishedAt
+ status
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all deployments of the environment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(4)
+ end
+
+ context 'when query last deployment' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(1)
+ expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query latest upcoming deployment' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(1)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query finished deployments in descending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+ expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s)
+ end
+ end
+
+ context 'when query finished deployments in ascending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s)
+ expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query upcoming deployments in descending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+ end
+ end
+
+ context 'when query upcoming deployments in ascending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+ expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query last deployments of multiple environments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environments {
+ nodes {
+ name
+ deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns an error for preventing N+1 queries' do
+ expect(subject['errors'][0]['message'])
+ .to include('"deployments" field can be requested only for 1 Environment(s) at a time.')
+ end
+ end
+
+ context 'when query finished and upcoming deployments together' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED SUCCESS]) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError)
+ end
+ end
+
+ context 'when multiple orderBy input are specified' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'raises an error' do
+ expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.')
+ end
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['environment']).to be_nil
+ end
+ end
+
+ shared_examples_for 'avoids N+1 database queries' do
+ it 'does not increase the query count' do
+ create_deployments
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ create_deployments
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+
+ def create_deployments
+ create_list(:deployment, 3, environment: environment, project: project).each do |deployment|
+ deployment.user = create(:user).tap { |u| project.add_developer(u) }
+ deployment.deployable =
+ create(:ci_build, project: project, environment: environment.name, deployment: deployment,
+ user: deployment.user)
+
+ deployment.save!
+ end
+ end
+ end
+
+ context 'when requesting commits of deployments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ commit {
+ author {
+ avatarUrl
+ name
+ webPath
+ }
+ fullTitle
+ webPath
+ sha
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns commits of deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment_in_record.sha).to eq(deployment['commit']['sha'])
+ end
+ end
+ end
+
+ context 'when requesting triggerers of deployments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ triggerer {
+ id
+ avatarUrl
+ name
+ webPath
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns triggerers of deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment_in_record.deployed_by.name).to eq(deployment['triggerer']['name'])
+ end
+ end
+ end
+
+ context 'when requesting jobs of deployments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ job {
+ id
+ status
+ name
+ webPath
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns jobs of deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment_in_record.build.to_global_id.to_s).to eq(deployment['job']['id'])
+ end
+ end
+ end
+
+ describe 'sorting and pagination' do
+ let(:data_path) { [:project, :environment, :deployments] }
+ let(:current_user) { user }
+
+ def pagination_query(params)
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS], #{params}) {
+ nodes {
+ iid
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |deployment| deployment['iid'].to_i }
+ end
+
+ context 'when sorting by finished_at in ascending order' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) }
+ let(:first_param) { 2 }
+ let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] }
+ end
+ end
+
+ context 'when sorting by finished_at in descending order' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) }
+ let(:first_param) { 2 }
+ let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index bab8d5b770c..5f8becc0726 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -156,13 +156,20 @@ RSpec.describe 'getting group members information' do
expect_array_response(child_user)
end
- it 'returns invited members plus inherited members' do
+ it 'returns invited members and inherited members of a shared group' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] })
expect(graphql_errors).to be_nil
expect_array_response(invited_user, user_1, user_2, child_user)
end
+ it 'returns invited members and inherited members of an ancestor of a shared group' do
+ fetch_members(group: grandchild_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(grandchild_user, invited_user, user_1, user_2, child_user)
+ end
+
it 'returns direct and inherited members' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
diff --git a/spec/requests/api/graphql/group/packages_spec.rb b/spec/requests/api/graphql/group/packages_spec.rb
index adee556db3a..cf8736db5af 100644
--- a/spec/requests/api/graphql/group/packages_spec.rb
+++ b/spec/requests/api/graphql/group/packages_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'getting a package list for a group' do
it 'returns an error for the second group and data for the first' do
expect(a_packages_names).to contain_exactly(group_one_package.name)
- expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/]
+ expect_graphql_errors_to_include [/"packages" field can be requested only for 1 Group\(s\) at a time./]
expect(graphql_data_at(:b, :packages)).to be(nil)
end
end
diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb
index a33e3ae5427..d6b0673e4f8 100644
--- a/spec/requests/api/graphql/group/work_item_types_spec.rb
+++ b/spec/requests/api/graphql/group/work_item_types_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'getting a list of work item types for a group' do
end
end
- context "when user doesn't have acces to the group" do
+ context "when user doesn't have access to the group" do
let(:current_user) { create(:user) }
before do
diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
index 46ec22e7ef8..06093e9f7c2 100644
--- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -100,6 +100,20 @@ RSpec.describe 'Reposition and move issue within board lists' do
expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title)
end
end
+
+ context 'when moving an issue using position_in_list' do
+ let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } }
+
+ it 'repositions an issue' do
+ post_graphql_mutation(mutation(params), current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ response_issue = json_response['data'][mutation_result_identifier]['issue']
+ expect(response_issue['iid']).to eq(issue1.iid.to_s)
+ expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title)
+ expect(response_issue['relativePosition']).to be < existing_issue1.relative_position
+ end
+ end
end
context 'when user has no access to resources' do
diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb
index 6a098002963..9ee2f41e8fc 100644
--- a/spec/requests/api/graphql/mutations/branches/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb
@@ -5,26 +5,18 @@ require 'spec_helper'
RSpec.describe 'Creation of a new branch' do
include GraphqlHelpers
+ let_it_be(:group) { create(:group, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:project) { create(:project, :public, :empty_repo) }
let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } }
- let(:new_branch) { 'new_branch' }
+ let(:new_branch) { "new_branch_#{SecureRandom.hex(4)}" }
let(:ref) { 'master' }
let(:mutation) { graphql_mutation(:create_branch, input) }
let(:mutation_response) { graphql_mutation_response(:create_branch) }
- context 'the user is not allowed to create a branch' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user has permissions to create a branch' do
- before do
- project.add_developer(current_user)
- end
-
- it 'creates a new branch' do
+ shared_examples 'creates a new branch' do
+ specify do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
@@ -33,14 +25,75 @@ RSpec.describe 'Creation of a new branch' do
'commit' => a_hash_including('id')
)
end
+ end
+
+ context 'when project is public' do
+ let_it_be(:project) { create(:project, :public, :empty_repo) }
+
+ context 'when user is not allowed to create a branch' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user is a direct project member' do
+ context 'and user is a developer' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'creates a new branch'
+
+ context 'when ref is not correct' do
+ err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\''
+ let(:new_branch) { 'another_branch' }
+ let(:ref) { 'unknown' }
+
+ it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg]
+ end
+ end
+ end
+
+ context 'when user is an inherited member from the group' do
+ context 'when project has a private repository' do
+ let_it_be(:project) { create(:project, :public, :empty_repo, :repository_private, group: group) }
+
+ context 'and user is a guest' do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'and user is a developer' do
+ before do
+ group.add_developer(current_user)
+ end
+
+ it_behaves_like 'creates a new branch'
+ end
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private, :empty_repo, group: group) }
+
+ context 'when user is an inherited member from the group' do
+ context 'and user is a guest' do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
- context 'when ref is not correct' do
- err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\''
- let(:new_branch) { 'another_branch' }
- let(:ref) { 'unknown' }
+ context 'and user is a developer' do
+ before do
+ group.add_developer(current_user)
+ end
- it_behaves_like 'a mutation that returns errors in the response',
- errors: [err_msg]
+ it_behaves_like 'creates a new branch'
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
new file mode 100644
index 00000000000..5855eb6bb51
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'JobArtifactsDestroy' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:job) { create(:ci_build) }
+
+ let(:mutation) do
+ variables = {
+ id: job.to_global_id.to_s
+ }
+ graphql_mutation(:job_artifacts_destroy, variables, <<~FIELDS)
+ job {
+ name
+ }
+ destroyedArtifactsCount
+ errors
+ FIELDS
+ end
+
+ before do
+ create(:ci_job_artifact, :archive, job: job)
+ create(:ci_job_artifact, :junit, job: job)
+ end
+
+ it 'returns an error if the user is not allowed to destroy the job artifacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ expect(job.reload.job_artifacts.count).to be(2)
+ end
+
+ it 'destroys the job artifacts and returns the expected data' do
+ job.project.add_maintainer(user)
+ expected_data = {
+ 'jobArtifactsDestroy' => {
+ 'errors' => [],
+ 'destroyedArtifactsCount' => 2,
+ 'job' => {
+ 'name' => job.name
+ }
+ }
+ }
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_data).to eq(expected_data)
+ expect(job.reload.job_artifacts.count).to be(0)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
new file mode 100644
index 00000000000..a5ec9ea343d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ArtifactDestroy' do
+ include GraphqlHelpers
+
+ let(:user) { create(:user) }
+ let(:artifact) { create(:ci_job_artifact) }
+
+ let(:mutation) do
+ variables = {
+ id: artifact.to_global_id.to_s
+ }
+ graphql_mutation(:artifact_destroy, variables, 'errors')
+ end
+
+ it 'returns an error if the user is not allowed to destroy the artifact' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ context 'when the user is allowed to destroy the artifact' do
+ before do
+ artifact.job.project.add_maintainer(user)
+ end
+
+ it 'destroys the artifact' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'returns error if destory fails' do
+ allow_next_found_instance_of(Ci::JobArtifact) do |instance|
+ allow(instance).to receive(:destroy).and_return(false)
+ allow(instance).to receive_message_chain(:errors, :full_messages).and_return(['cannot be removed'])
+ end
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:artifact_destroy, :errors)).to contain_exactly('cannot be removed')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
index c91437fa355..66facdebe78 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -39,5 +39,19 @@ RSpec.describe 'Creation of a new Custom Emoji' do
expect(gql_response['customEmoji']['name']).to eq(attributes[:name])
expect(gql_response['customEmoji']['url']).to eq(attributes[:url])
end
+
+ context 'when the custom_emoji feature flag is disabled' do
+ before do
+ stub_feature_flags(custom_emoji: false)
+ end
+
+ it 'does nothing and returns and error' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change(CustomEmoji, :count)
+
+ expect_graphql_errors_to_include('Custom emoji feature is disabled')
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
index 07fd57a2cee..7d25206e617 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
@@ -68,6 +68,20 @@ RSpec.describe 'Deletion of custom emoji' do
end
it_behaves_like 'deletes custom emoji'
+
+ context 'when the custom_emoji feature flag is disabled' do
+ before do
+ stub_feature_flags(custom_emoji: false)
+ end
+
+ it_behaves_like 'does not delete custom emoji'
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('Custom emoji feature is disabled')
+ end
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
index 9272e218172..85eaec90f47 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Promote an incident timeline event from a comment' do
include GraphqlHelpers
+ include NotesHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -53,7 +54,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do
'promotedFromNote' => {
'id' => comment.to_global_id.to_s
},
- 'note' => comment.note,
+ 'note' => "@#{comment.author.username} [commented](#{noteable_note_url(comment)}): '#{comment.note}'",
'action' => 'comment',
'editable' => true,
'occurredAt' => comment.created_at.iso8601
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
index 608b36e4f15..8cec5867aca 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -93,6 +93,16 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled do
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
end
+
+ it 'triggers webhooks', :sidekiq_inline do
+ hook = create(:project_hook, merge_requests_events: true, project: merge_request.project)
+
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
end
context 'when passing an empty list of assignees' do
diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb
index 33d4e57904c..240db764f40 100644
--- a/spec/requests/api/graphql/mutations/releases/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb
@@ -22,9 +22,14 @@ RSpec.describe 'Updating an existing release' do
let_it_be(:milestones) { [milestone_12_3, milestone_12_4] }
let_it_be(:release) do
- create(:release, project: project, tag: tag_name, name: name,
- description: description, released_at: Time.parse(released_at).utc,
- created_at: Time.parse(created_at).utc, milestones: milestones)
+ create(:release,
+ project: project,
+ tag: tag_name,
+ name: name,
+ description: description,
+ released_at: Time.parse(released_at).utc,
+ created_at: Time.parse(created_at).utc,
+ milestones: milestones)
end
let(:mutation_name) { :release_update }
diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb
index 9830623ede8..89c01d44771 100644
--- a/spec/requests/api/graphql/packages/composer_spec.rb
+++ b/spec/requests/api/graphql/packages/composer_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:composer_package, project: project) }
+ let_it_be(:package) { create(:composer_package, :last_downloaded_at, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb
index 5bd5a71bbeb..7ad85edecef 100644
--- a/spec/requests/api/graphql/packages/conan_spec.rb
+++ b/spec/requests/api/graphql/packages/conan_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'conan package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:conan_package, project: project) }
+ let_it_be(:package) { create(:conan_package, :last_downloaded_at, project: project) }
let(:metadata) { query_graphql_fragment('ConanMetadata') }
let(:package_files_metadata) { query_graphql_fragment('ConanFileMetadata') }
diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb
index 1675b8faa23..79a589e2dc2 100644
--- a/spec/requests/api/graphql/packages/helm_spec.rb
+++ b/spec/requests/api/graphql/packages/helm_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'helm package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:helm_package, project: project) }
+ let_it_be(:package) { create(:helm_package, :last_downloaded_at, project: project) }
let(:package_files_metadata) { query_graphql_fragment('HelmFileMetadata') }
diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb
index 9d59a922660..b7f39efcf73 100644
--- a/spec/requests/api/graphql/packages/maven_spec.rb
+++ b/spec/requests/api/graphql/packages/maven_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'maven package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:maven_package, project: project) }
+ let_it_be(:package) { create(:maven_package, :last_downloaded_at, project: project) }
let(:metadata) { query_graphql_fragment('MavenMetadata') }
@@ -31,7 +31,9 @@ RSpec.describe 'maven package details' do
context 'a versionless maven package' do
let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) }
- let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
+ let_it_be(:package) do
+ create(:maven_package, :last_downloaded_at, project: project, version: nil, maven_metadatum: maven_metadatum)
+ end
subject { post_graphql(query, current_user: user) }
diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb
index 87cffc67ce5..7de132d1574 100644
--- a/spec/requests/api/graphql/packages/nuget_spec.rb
+++ b/spec/requests/api/graphql/packages/nuget_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'nuget package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) }
+ let_it_be(:package) { create(:nuget_package, :last_downloaded_at, :with_metadatum, project: project) }
let_it_be(:dependency_link) { create(:packages_dependency_link, :with_nuget_metadatum, package: package) }
let(:metadata) { query_graphql_fragment('NugetMetadata') }
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index c28b37db5af..e9f82d66775 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'package details' do
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be_with_reload(:composer_package) { create(:composer_package, :last_downloaded_at, project: project) }
let_it_be(:user) { create(:user) }
- let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
@@ -65,6 +65,17 @@ RSpec.describe 'package details' do
end
end
+ context 'with package without last_downloaded_at' do
+ before do
+ composer_package.update!(last_downloaded_at: nil)
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
context 'with package files pending destruction' do
let_it_be(:package_file) { create(:package_file, package: composer_package) }
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
@@ -97,7 +108,7 @@ RSpec.describe 'package details' do
expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
- expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
+ expect_graphql_errors_to_include [/"package" field can be requested only for 1 Query\(s\) at a time./]
expect(graphql_data_at(:b)).to be(nil)
end
end
diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb
index 0cc5bd2e3b2..c0e589f3597 100644
--- a/spec/requests/api/graphql/packages/pypi_spec.rb
+++ b/spec/requests/api/graphql/packages/pypi_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'pypi package details' do
include GraphqlHelpers
include_context 'package details setup'
- let_it_be(:package) { create(:pypi_package, project: project) }
+ let_it_be(:package) { create(:pypi_package, :last_downloaded_at, project: project) }
let(:metadata) { query_graphql_fragment('PypiMetadata') }
diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
new file mode 100644
index 00000000000..cb5006ec8e4
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting merge access levels for a branch protection' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:merge_access_level_data) { merge_access_levels_data[0] }
+
+ let(:merge_access_levels_data) do
+ graphql_data_at('project',
+ 'branchRules',
+ 'nodes',
+ 0,
+ 'branchProtection',
+ 'mergeAccessLevels',
+ 'nodes')
+ end
+
+ let(:project) { protected_branch.project }
+
+ let(:merge_access_levels_count) { protected_branch.merge_access_levels.size }
+
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for('MergeAccessLevel') }
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ mergeAccessLevels {
+ nodes {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ let_it_be(:protected_branch) { create(:protected_branch) }
+
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(merge_access_levels_data).not_to be_present }
+ end
+
+ shared_examples 'merge access request' do
+ let(:merge_access) { protected_branch.merge_access_levels.first }
+
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all merge access levels' do
+ expect(merge_access_levels_data.size).to eq(merge_access_levels_count)
+ end
+
+ it 'includes access_level' do
+ expect(merge_access_level_data['accessLevel'])
+ .to eq(merge_access.access_level)
+ end
+
+ it 'includes access_level_description' do
+ expect(merge_access_level_data['accessLevelDescription'])
+ .to eq(merge_access.humanize)
+ end
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ let(:merge_access) { protected_branch.merge_access_levels.first }
+
+ context 'when no one has access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_merge) }
+
+ it_behaves_like 'merge access request'
+ end
+
+ context 'when developers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :developers_can_merge) }
+
+ it_behaves_like 'merge access request'
+ end
+
+ context 'when maintainers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_merge) }
+
+ it_behaves_like 'merge access request'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
new file mode 100644
index 00000000000..59f9c7d61cb
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting push access levels for a branch protection' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:push_access_level_data) { push_access_levels_data[0] }
+
+ let(:push_access_levels_data) do
+ graphql_data_at('project',
+ 'branchRules',
+ 'nodes',
+ 0,
+ 'branchProtection',
+ 'pushAccessLevels',
+ 'nodes')
+ end
+
+ let(:project) { protected_branch.project }
+
+ let(:push_access_levels_count) { protected_branch.push_access_levels.size }
+
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for('PushAccessLevel'.classify) }
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ pushAccessLevels {
+ nodes {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ let_it_be(:protected_branch) { create(:protected_branch) }
+
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(push_access_levels_data).not_to be_present }
+ end
+
+ shared_examples 'push access request' do
+ let(:push_access) { protected_branch.push_access_levels.first }
+
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all push access levels' do
+ expect(push_access_levels_data.size).to eq(push_access_levels_count)
+ end
+
+ it 'includes access_level' do
+ expect(push_access_level_data['accessLevel'])
+ .to eq(push_access.access_level)
+ end
+
+ it 'includes access_level_description' do
+ expect(push_access_level_data['accessLevelDescription'])
+ .to eq(push_access.humanize)
+ end
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ let(:push_access) { protected_branch.push_access_levels.first }
+
+ context 'when no one has access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_push) }
+
+ it_behaves_like 'push access request'
+ end
+
+ context 'when developers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :developers_can_push) }
+
+ it_behaves_like 'push access request'
+ end
+
+ context 'when maintainers have access' do
+ let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_push) }
+
+ it_behaves_like 'push access request'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
new file mode 100644
index 00000000000..8a3f546ef95
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting branch protection for a branch rule' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:branch_rule) { create(:protected_branch) }
+ let_it_be(:project) { branch_rule.project }
+
+ let(:branch_protection_data) do
+ graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection')
+ end
+
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for('BranchProtection') }
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(branch_protection_data).not_to be_present }
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'includes allow_force_push' do
+ expect(branch_protection_data['allowForcePush']).to be_in([true, false])
+ expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb
new file mode 100644
index 00000000000..70fb37941e2
--- /dev/null
+++ b/spec/requests/api/graphql/project/branch_rules_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting list of branch rules for a project' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:branch_name_a) { 'branch_name_a' }
+ let_it_be(:branch_name_b) { 'wildcard-*' }
+ let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] }
+
+ let_it_be(:branch_rule_a) do
+ create(:protected_branch, project: project, name: branch_name_a)
+ end
+
+ let_it_be(:branch_rule_b) do
+ create(:protected_branch, project: project, name: branch_name_b)
+ end
+
+ let(:branch_rules_data) { graphql_data_at('project', 'branchRules', 'edges') }
+ let(:variables) { { path: project.full_path } }
+
+ let(:fields) do
+ <<~QUERY
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ }
+ edges {
+ cursor
+ node {
+ #{all_graphql_fields_for('branch_rules'.classify)}
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $n: Int, $cursor: String) {
+ project(fullPath: $path) {
+ branchRules(first: $n, after: $cursor) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ context 'when the user does not have read_protected_branch abilities' do
+ before do
+ project.add_guest(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(branch_rules_data).to be_empty }
+ end
+
+ context 'when the user does have read_protected_branch abilities' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'includes a name' do
+ expect(branch_rules_data.dig(0, 'node', 'name')).to be_present
+ end
+
+ it 'includes created_at and updated_at' do
+ expect(branch_rules_data.dig(0, 'node', 'createdAt')).to be_present
+ expect(branch_rules_data.dig(1, 'node', 'updatedAt')).to be_present
+ end
+
+ context 'when limiting the number of results' do
+ let(:branch_rule_limit) { 1 }
+ let(:variables) { { path: project.full_path, n: branch_rule_limit } }
+ let(:next_variables) do
+ { path: project.full_path, n: branch_rule_limit, cursor: last_cursor }
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'only returns N branch_rules' do
+ expect(branch_rules_data.size).to eq(branch_rule_limit)
+ expect(has_next_page).to be_truthy
+ expect(has_prev_page).to be_falsey
+ post_graphql(query, current_user: current_user, variables: next_variables)
+ expect(branch_rules_data.size).to eq(branch_rule_limit)
+ expect(has_next_page).to be_falsey
+ expect(has_prev_page).to be_truthy
+ end
+ end
+
+ context 'when no limit is provided' do
+ let(:branch_rule_limit) { nil }
+
+ it 'returns all branch_rules' do
+ expect(branch_rules_data.size).to eq(branch_rules.size)
+ end
+ end
+ end
+ end
+
+ def pagination_info
+ graphql_data_at('project', 'branchRules', 'pageInfo')
+ end
+
+ def has_next_page
+ pagination_info['hasNextPage']
+ end
+
+ def has_prev_page
+ pagination_info['hasPreviousPage']
+ end
+
+ def last_cursor
+ branch_rules_data.last['cursor']
+ end
+end
diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb
new file mode 100644
index 00000000000..e5ef7bcafbf
--- /dev/null
+++ b/spec/requests/api/graphql/project/deployment_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Deployment query' do
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, environment: environment, project: project) }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ let(:user) { developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ deployment(iid: #{deployment.iid}) {
+ id
+ iid
+ ref
+ tag
+ sha
+ createdAt
+ updatedAt
+ finishedAt
+ status
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the deployment of the project' do
+ deployment_data = subject.dig('data', 'project', 'deployment')
+
+ expect(deployment_data['iid']).to eq(deployment.iid.to_s)
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it 'returns nothing' do
+ deployment_data = subject.dig('data', 'project', 'deployment')
+
+ expect(deployment_data).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
new file mode 100644
index 00000000000..e5b6aebbf2c
--- /dev/null
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Environments query' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be_with_refind(:production) { create(:environment, :production, project: project) }
+ let_it_be_with_refind(:staging) { create(:environment, :staging, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ subject { post_graphql(query, current_user: user) }
+
+ let(:user) { developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{production.name}") {
+ slug
+ createdAt
+ updatedAt
+ autoStopAt
+ autoDeleteAt
+ tier
+ environmentType
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the specified fields of the environment', :aggregate_failures do
+ production.update!(auto_stop_at: 1.day.ago, auto_delete_at: 2.days.ago, environment_type: 'review')
+
+ subject
+
+ environment_data = graphql_data.dig('project', 'environment')
+ expect(environment_data['slug']).to eq(production.slug)
+ expect(environment_data['createdAt']).to eq(production.created_at.iso8601)
+ expect(environment_data['updatedAt']).to eq(production.updated_at.iso8601)
+ expect(environment_data['autoStopAt']).to eq(production.auto_stop_at.iso8601)
+ expect(environment_data['autoDeleteAt']).to eq(production.auto_delete_at.iso8601)
+ expect(environment_data['tier']).to eq(production.tier.upcase)
+ expect(environment_data['environmentType']).to eq(production.environment_type)
+ end
+
+ describe 'last deployments of environments' do
+ ::Deployment.statuses.each do |status, _|
+ let_it_be(:"production_#{status}_deployment") do
+ create(:deployment, status.to_sym, environment: production, project: project)
+ end
+
+ let_it_be(:"staging_#{status}_deployment") do
+ create(:deployment, status.to_sym, environment: staging, project: project)
+ end
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environments {
+ nodes {
+ name
+ lastSuccessDeployment: lastDeployment(status: SUCCESS) {
+ iid
+ }
+ lastRunningDeployment: lastDeployment(status: RUNNING) {
+ iid
+ }
+ lastBlockedDeployment: lastDeployment(status: BLOCKED) {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all last deployments of the environment' do
+ subject
+
+ environments_data = graphql_data_at(:project, :environments, :nodes)
+
+ environments_data.each do |environment_data|
+ name = environment_data['name']
+ success_deployment = public_send(:"#{name}_success_deployment")
+ running_deployment = public_send(:"#{name}_running_deployment")
+ blocked_deployment = public_send(:"#{name}_blocked_deployment")
+
+ expect(environment_data['lastSuccessDeployment']['iid']).to eq(success_deployment.iid.to_s)
+ expect(environment_data['lastRunningDeployment']['iid']).to eq(running_deployment.iid.to_s)
+ expect(environment_data['lastBlockedDeployment']['iid']).to eq(blocked_deployment.iid.to_s)
+ end
+ end
+
+ it 'executes the same number of queries in single environment and multiple environments' do
+ single_environment_query =
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{production.name}") {
+ name
+ lastSuccessDeployment: lastDeployment(status: SUCCESS) {
+ iid
+ }
+ lastRunningDeployment: lastDeployment(status: RUNNING) {
+ iid
+ }
+ lastBlockedDeployment: lastDeployment(status: BLOCKED) {
+ iid
+ }
+ }
+ }
+ }
+ )
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(single_environment_query, context: { current_user: user })
+ end
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index 8cda61f0628..0444ce43c22 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -11,14 +11,14 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
let_it_be(:developer) { create(:user) }
let_it_be(:stranger) { create(:user) }
let_it_be(:old_version) do
- create(:design_version, issue: issue,
- created_designs: create_list(:design, 3, issue: issue))
+ create(:design_version, issue: issue, created_designs: create_list(:design, 3, issue: issue))
end
let_it_be(:version) do
- create(:design_version, issue: issue,
- modified_designs: old_version.designs,
- created_designs: create_list(:design, 2, issue: issue))
+ create(:design_version,
+ issue: issue,
+ modified_designs: old_version.designs,
+ created_designs: create_list(:design, 2, issue: issue))
end
let(:current_user) { developer }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 596e023a027..28282860416 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -27,14 +27,6 @@ RSpec.describe 'getting an issue list for a project' do
QUERY
end
- let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('issues', issue_filter_params, fields)
- )
- end
-
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
@@ -89,6 +81,14 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'when filtering by search' do
+ it_behaves_like 'query with a search term' do
+ let(:issuable_data) { issues_data }
+ let(:user) { current_user }
+ let_it_be(:issuable) { create(:issue, project: project, description: 'bar') }
+ end
+ end
+
context 'when limiting the number of results' do
let(:query) do
<<~GQL
@@ -301,7 +301,7 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
+ it_behaves_like 'sorted paginated query', is_reversible: true do
let(:sort_param) { :RELATIVE_POSITION_ASC }
let(:first_param) { 2 }
let(:all_records) do
@@ -679,4 +679,12 @@ RSpec.describe 'getting an issue list for a project' do
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
+
+ def query(params = issue_filter_params)
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('issues', params, fields)
+ )
+ end
end
diff --git a/spec/requests/api/graphql/project/job_spec.rb b/spec/requests/api/graphql/project/job_spec.rb
new file mode 100644
index 00000000000..6edd4cf753f
--- /dev/null
+++ b/spec/requests/api/graphql/project/job_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project.job' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:job) { create(:ci_build, project: project, name: 'GQL test job') }
+
+ let(:query) do
+ <<~QUERY
+ {
+ project(fullPath: "#{project.full_path}") {
+ job(id: "#{job.to_global_id}") {
+ name
+ }
+ }
+ }
+ QUERY
+ end
+
+ context 'when the user can read jobs on the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns the job that matches the given ID' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'job', 'name')).to eq('GQL test job')
+ end
+
+ context 'when no job matches the given ID' do
+ let(:job) { create(:ci_build, project: create(:project), name: 'Job from another project') }
+
+ it 'returns null' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'job')).to be_nil
+ end
+ end
+ end
+
+ context 'when the user cannot read jobs on the project' do
+ it 'returns null' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'job')).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index d2f34080be3..6a59df81405 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -365,7 +365,7 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(interaction_data).to contain_exactly a_hash_including(
'canMerge' => false,
'canUpdate' => can_update,
- 'reviewState' => attention_requested,
+ 'reviewState' => unreviewed,
'reviewed' => false,
'approved' => false
)
@@ -398,8 +398,8 @@ RSpec.describe 'getting merge request information nested in a project' do
describe 'scalability' do
let_it_be(:other_users) { create_list(:user, 3) }
- let(:attention_requested) do
- { 'reviewState' => 'ATTENTION_REQUESTED' }
+ let(:unreviewed) do
+ { 'reviewState' => 'UNREVIEWED' }
end
let(:reviewed) do
@@ -425,15 +425,15 @@ RSpec.describe 'getting merge request information nested in a project' do
other_users.each do |user|
assign_user(user)
- merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user, state: :attention_requested)
+ merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user)
end
expect { post_graphql(query) }.not_to exceed_query_limit(baseline)
expect(interaction_data).to contain_exactly(
- include(attention_requested),
- include(attention_requested),
- include(attention_requested),
+ include(unreviewed),
+ include(unreviewed),
+ include(unreviewed),
include(reviewed)
)
end
@@ -462,17 +462,17 @@ RSpec.describe 'getting merge request information nested in a project' do
it_behaves_like 'when requesting information about MR interactions' do
let(:field) { :reviewers }
- let(:attention_requested) { 'ATTENTION_REQUESTED' }
+ let(:unreviewed) { 'UNREVIEWED' }
let(:can_update) { false }
def assign_user(user)
- merge_request.merge_request_reviewers.create!(reviewer: user, state: :attention_requested)
+ merge_request.merge_request_reviewers.create!(reviewer: user)
end
end
it_behaves_like 'when requesting information about MR interactions' do
let(:field) { :assignees }
- let(:attention_requested) { nil }
+ let(:unreviewed) { nil }
let(:can_update) { true } # assignees can update MRs
def assign_user(user)
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index 08c6a2d9927..41915d3cdee 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe 'getting pipeline information nested in a project' do
name: build_job.name,
pipeline: pipeline,
stage_idx: 0,
- stage: build_job.stage)
+ stage: build_job.stage_name)
end
let(:fields) do
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
index 8f2d2cffef2..5e207ec0963 100644
--- a/spec/requests/api/graphql/project/terraform/state_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -60,17 +60,17 @@ RSpec.describe 'query a single terraform state' do
expect(data).to match a_graphql_entity_for(
terraform_state,
:name,
- 'lockedAt' => terraform_state.locked_at.iso8601,
- 'createdAt' => terraform_state.created_at.iso8601,
- 'updatedAt' => terraform_state.updated_at.iso8601,
- 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
'latestVersion' => a_graphql_entity_for(
latest_version,
- 'serial' => eq(latest_version.version),
- 'createdAt' => eq(latest_version.created_at.iso8601),
- 'updatedAt' => eq(latest_version.updated_at.iso8601),
+ 'serial' => eq(latest_version.version),
+ 'createdAt' => eq(latest_version.created_at.iso8601),
+ 'updatedAt' => eq(latest_version.updated_at.iso8601),
'createdByUser' => a_graphql_entity_for(latest_version.created_by_user),
- 'job' => { 'name' => eq(latest_version.build.name) }
+ 'job' => { 'name' => eq(latest_version.build.name) }
)
)
end
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index a7ec6f69776..cc3660bcc6b 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -64,18 +64,18 @@ RSpec.describe 'query terraform states' do
expect(data['nodes']).to contain_exactly a_graphql_entity_for(
terraform_state, :name,
- 'lockedAt' => terraform_state.locked_at.iso8601,
- 'createdAt' => terraform_state.created_at.iso8601,
- 'updatedAt' => terraform_state.updated_at.iso8601,
- 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user),
'latestVersion' => a_graphql_entity_for(
latest_version,
- 'serial' => eq(latest_version.version),
- 'downloadPath' => eq(download_path),
- 'createdAt' => eq(latest_version.created_at.iso8601),
- 'updatedAt' => eq(latest_version.updated_at.iso8601),
+ 'serial' => eq(latest_version.version),
+ 'downloadPath' => eq(download_path),
+ 'createdAt' => eq(latest_version.created_at.iso8601),
+ 'updatedAt' => eq(latest_version.updated_at.iso8601),
'createdByUser' => a_graphql_entity_for(latest_version.created_by_user),
- 'job' => { 'name' => eq(latest_version.build.name) }
+ 'job' => { 'name' => eq(latest_version.build.name) }
)
)
end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index 6ef28392b8b..69f8d1cac74 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -10,7 +10,10 @@ RSpec.describe 'getting an work item list for a project' do
let_it_be(:current_user) { create(:user) }
let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1') }
- let_it_be(:item2) { create(:work_item, project: project, title: 'item2') }
+ let_it_be(:item2) do
+ create(:work_item, project: project, title: 'item2', last_edited_by: current_user, last_edited_at: 1.day.ago)
+ end
+
let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') }
let_it_be(:other_item) { create(:work_item) }
@@ -27,14 +30,6 @@ RSpec.describe 'getting an work item list for a project' do
QUERY
end
- let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('workItems', item_filter_params, fields)
- )
- end
-
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
@@ -83,6 +78,48 @@ RSpec.describe 'getting an work item list for a project' do
end
end
+ context 'when fetching description edit information' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ edited
+ lastEditedAt
+ lastEditedBy {
+ webPath
+ username
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'avoids N+1 queries' do
+ post_graphql(query, current_user: current_user) # warm-up
+
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+ expect_graphql_errors_to_be_empty
+
+ create_list(:work_item, 3, :last_edited_by_user, last_edited_at: 1.week.ago, project: project)
+
+ expect_graphql_errors_to_be_empty
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
+ end
+ end
+
+ context 'when filtering by search' do
+ it_behaves_like 'query with a search term' do
+ let(:issuable_data) { items_data }
+ let(:user) { current_user }
+ let_it_be(:issuable) { create(:work_item, project: project, description: 'bar') }
+ end
+ end
+
describe 'sorting and pagination' do
let(:data_path) { [:project, :work_items] }
@@ -118,4 +155,12 @@ RSpec.describe 'getting an work item list for a project' do
def item_ids
graphql_dig_at(items_data, :node, :id)
end
+
+ def query(params = item_filter_params)
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('workItems', params, fields)
+ )
+ end
end
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index 4aa9c4b8254..359c599cd3a 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -108,8 +108,8 @@ RSpec.describe 'Query' do
design_at_version,
'filename' => design_at_version.design.filename,
'version' => a_graphql_entity_for(version, :sha),
- 'design' => a_graphql_entity_for(design),
- 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s },
+ 'design' => a_graphql_entity_for(design),
+ 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s },
'project' => a_graphql_entity_for(project, :full_path)
)
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 34644e5893a..e4bb4109c76 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -14,7 +14,10 @@ RSpec.describe 'Query.work_item(id)' do
project: project,
description: '- List item',
start_date: Date.today,
- due_date: 1.week.from_now
+ due_date: 1.week.from_now,
+ created_at: 1.week.ago,
+ last_edited_at: 1.day.ago,
+ last_edited_by: guest
)
end
@@ -67,6 +70,12 @@ RSpec.describe 'Query.work_item(id)' do
... on WorkItemWidgetDescription {
description
descriptionHtml
+ edited
+ lastEditedBy {
+ webPath
+ username
+ }
+ lastEditedAt
}
}
GRAPHQL
@@ -79,7 +88,13 @@ RSpec.describe 'Query.work_item(id)' do
hash_including(
'type' => 'DESCRIPTION',
'description' => work_item.description,
- 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {})
+ 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}),
+ 'edited' => true,
+ 'lastEditedAt' => work_item.last_edited_at.iso8601,
+ 'lastEditedBy' => {
+ 'webPath' => "/#{guest.full_path}",
+ 'username' => guest.username
+ }
)
)
)
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index bda46f85140..83c34204c78 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe API::GroupExport do
before do
allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
allow(strategy).to receive(:increment).and_return(0)
+ allow(strategy).to receive(:read).and_return(0)
end
upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index bc37f8e4655..6169bc9b2a2 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Groups do
include GroupAPIHelpers
include UploadHelpers
+ include WorkhorseHelpers
let_it_be(:user1) { create(:user, can_create_group: false) }
let_it_be(:user2) { create(:user) }
@@ -540,9 +541,9 @@ RSpec.describe API::Groups do
# Returns a Hash of visibility_level => Project pairs
def add_projects_to_group(group, share_with: nil)
projects = {
- public: create(:project, :public, namespace: group),
+ public: create(:project, :public, namespace: group),
internal: create(:project, :internal, namespace: group),
- private: create(:project, :private, namespace: group)
+ private: create(:project, :private, namespace: group)
}
if share_with
@@ -872,21 +873,31 @@ RSpec.describe API::Groups do
group_param = {
avatar: fixture_file_upload(file_path)
}
- put api("/groups/#{group1.id}", user1), params: group_param
+ workhorse_form_with_file(
+ api("/groups/#{group1.id}", user1),
+ method: :put,
+ file_key: :avatar,
+ params: group_param
+ )
end
end
context 'when authenticated as the group owner' do
it 'updates the group' do
- put api("/groups/#{group1.id}", user1), params: {
- name: new_group_name,
- request_access_enabled: true,
- project_creation_level: "noone",
- subgroup_creation_level: "maintainer",
- default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
- prevent_sharing_groups_outside_hierarchy: true,
- avatar: fixture_file_upload(file_path)
- }
+ workhorse_form_with_file(
+ api("/groups/#{group1.id}", user1),
+ method: :put,
+ file_key: :avatar,
+ params: {
+ name: new_group_name,
+ request_access_enabled: true,
+ project_creation_level: "noone",
+ subgroup_creation_level: "maintainer",
+ default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
+ prevent_sharing_groups_outside_hierarchy: true,
+ avatar: fixture_file_upload(file_path)
+ }
+ )
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(new_group_name)
@@ -912,6 +923,16 @@ RSpec.describe API::Groups do
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
end
+ it 'removes the group avatar' do
+ put api("/groups/#{group1.id}", user1), params: { avatar: '' }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to be_nil
+ expect(group1.reload.avatar_url).to be_nil
+ end
+ end
+
it 'does not update visibility_level if it is restricted' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
@@ -1787,7 +1808,12 @@ RSpec.describe API::Groups do
attrs[:avatar] = fixture_file_upload(file_path)
end
- post api("/groups", user3), params: params
+ workhorse_form_with_file(
+ api('/groups', user3),
+ method: :post,
+ file_key: :avatar,
+ params: params
+ )
end
end
@@ -2029,6 +2055,90 @@ RSpec.describe API::Groups do
end
end
+ describe 'GET /groups/:id/transfer_locations' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_group) { create(:group, :private) }
+
+ let(:params) { {} }
+
+ subject(:request) do
+ get api("/groups/#{source_group.id}/transfer_locations", user), params: params
+ end
+
+ context 'when the user has rights to transfer the group' do
+ let_it_be(:guest_group) { create(:group) }
+ let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') }
+ let_it_be(:owner_group_1) { create(:group, name: 'owner group', path: 'owner-group') }
+ let_it_be(:owner_group_2) { create(:group, name: 'gitlab group', path: 'gitlab-group') }
+ let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) }
+
+ before do
+ source_group.add_owner(user)
+ guest_group.add_guest(user)
+ maintainer_group.add_maintainer(user)
+ owner_group_1.add_owner(user)
+ owner_group_2.add_owner(user)
+ create(:group_group_link, :owner,
+ shared_with_group: owner_group_1,
+ shared_group: shared_with_group_where_direct_owner_as_owner
+ )
+ end
+
+ it 'returns 200' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ end
+
+ it 'only includes groups where the user has permissions to transfer a group to' do
+ request
+
+ expect(group_ids_from_response).to contain_exactly(
+ owner_group_1.id,
+ owner_group_2.id,
+ shared_with_group_where_direct_owner_as_owner.id
+ )
+ end
+
+ context 'with search' do
+ let(:params) { { search: 'gitlab' } }
+
+ it 'includes groups where the user has permissions to transfer a group to, matching the search term' do
+ request
+
+ expect(group_ids_from_response).to contain_exactly(owner_group_2.id)
+ end
+ end
+
+ def group_ids_from_response
+ json_response.map { |group| group['id'] }
+ end
+ end
+
+ context 'when the user does not have permissions to transfer the group' do
+ before do
+ source_group.add_developer(user)
+ end
+
+ it 'returns 403' do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'for an anonymous user' do
+ let_it_be(:user) { nil }
+
+ it 'returns 404' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'POST /groups/:id/transfer' do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:new_parent_group) { create(:group, :private) }
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 7de72de3940..d2fa3dabe69 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -13,15 +13,15 @@ RSpec.describe API::ImportGithub do
let(:provider_username) { user.username }
let(:provider_user) { double('provider', login: provider_username) }
let(:provider_repo) do
- double('provider',
+ {
name: 'vim',
full_name: "#{provider_username}/vim",
owner: double('provider', login: provider_username),
description: 'provider',
private: false,
clone_url: 'https://fake.url/vim.git',
- has_wiki?: true
- )
+ has_wiki: true
+ }
end
before do
@@ -48,7 +48,7 @@ RSpec.describe API::ImportGithub do
it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post api("/import/github", user), params: {
@@ -63,7 +63,7 @@ RSpec.describe API::ImportGithub do
it 'returns 201 response when the project is imported successfully from GHE' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post api("/import/github", user), params: {
diff --git a/spec/requests/api/integrations/slack/events_spec.rb b/spec/requests/api/integrations/slack/events_spec.rb
deleted file mode 100644
index 176e9eded31..00000000000
--- a/spec/requests/api/integrations/slack/events_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::Integrations::Slack::Events do
- describe 'POST /integrations/slack/events' do
- let(:params) { {} }
- let(:headers) do
- {
- ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
- ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
- }
- end
-
- before do
- allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
- signature == 'mock_verified_signature'
- end
-
- stub_application_setting(slack_app_signing_secret: 'mock_key')
- end
-
- subject { post api('/integrations/slack/events'), params: params, headers: headers }
-
- shared_examples 'an unauthorized request' do
- specify do
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- shared_examples 'a successful request that generates a tracked error' do
- specify do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).once
-
- subject
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(response.body).to be_empty
- end
- end
-
- context 'when the slack_app_signing_secret setting is not set' do
- before do
- stub_application_setting(slack_app_signing_secret: nil)
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the timestamp header has expired' do
- before do
- headers[::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER] = 5.minutes.ago.to_i.to_s
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the timestamp header is missing' do
- before do
- headers.delete(::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER)
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the signature header is missing' do
- before do
- headers.delete(::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER)
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when the signature is not verified' do
- before do
- headers[::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER] = 'unverified_signature'
- end
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when type param is missing' do
- it_behaves_like 'a successful request that generates a tracked error'
- end
-
- context 'when type param is unknown' do
- let(:params) do
- { type: 'unknown_type' }
- end
-
- it_behaves_like 'a successful request that generates a tracked error'
- end
-
- context 'when type param is url_verification' do
- let(:params) do
- {
- type: 'url_verification',
- challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P'
- }
- end
-
- it 'responds in-request with the challenge' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'challenge' => '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' })
- end
- end
- end
-end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index e100684018a..1f6c241b3f5 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe API::Internal::Base do
+ include GitlabShellHelpers
include APIInternalBaseHelpers
let_it_be(:user, reload: true) { create(:user) }
@@ -17,10 +18,14 @@ RSpec.describe API::Internal::Base do
let(:snippet_changes) { "#{TestEnv::BRANCH_SHA['snippet/single-file']} #{TestEnv::BRANCH_SHA['snippet/edit-file']} refs/heads/snippet/edit-file" }
describe "GET /internal/check" do
+ def perform_request(headers: gitlab_shell_internal_api_request_header)
+ get api("/internal/check"), headers: headers
+ end
+
it do
expect_any_instance_of(Redis).to receive(:ping).and_return('PONG')
- get api("/internal/check"), params: { secret_token: secret_token }
+ perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['api_version']).to eq(API::API.version)
@@ -30,24 +35,57 @@ RSpec.describe API::Internal::Base do
it 'returns false for field `redis` when redis is unavailable' do
expect_any_instance_of(Redis).to receive(:ping).and_raise(Errno::ENOENT)
- get api("/internal/check"), params: { secret_token: secret_token }
+ perform_request
expect(json_response['redis']).to be(false)
end
context 'authenticating' do
- it 'authenticates using a header' do
- get api("/internal/check"),
- headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
+ it 'authenticates using a jwt token in a header' do
+ perform_request
expect(response).to have_gitlab_http_status(:ok)
end
- it 'returns 401 when no credentials provided' do
- get(api("/internal/check"))
+ it 'returns 401 when jwt token is expired' do
+ headers = gitlab_shell_internal_api_request_header
+
+ travel_to(2.minutes.since) do
+ perform_request(headers: headers)
+ end
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 401 when jwt issuer is not Gitlab-Shell' do
+ perform_request(headers: gitlab_shell_internal_api_request_header(issuer: "gitlab-workhorse"))
expect(response).to have_gitlab_http_status(:unauthorized)
end
+
+ it 'returns 401 when jwt token is not provided, even if plain secret is provided' do
+ perform_request(headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'when gitlab_shell_jwt_token is disabled' do
+ before do
+ stub_feature_flags(gitlab_shell_jwt_token: false)
+ end
+
+ it 'authenticates using a header' do
+ perform_request(headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns 401 when no credentials provided' do
+ get(api("/internal/check"))
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
end
@@ -56,10 +94,8 @@ RSpec.describe API::Internal::Base do
subject do
post api('/internal/two_factor_recovery_codes'),
- params: {
- secret_token: secret_token,
- key_id: key_id
- }
+ params: { key_id: key_id },
+ headers: gitlab_shell_internal_api_request_header
end
it_behaves_like 'actor key validations'
@@ -105,10 +141,8 @@ RSpec.describe API::Internal::Base do
subject do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- key_id: key_id
- }
+ params: { key_id: key_id },
+ headers: gitlab_shell_internal_api_request_header
end
it_behaves_like 'actor key validations'
@@ -126,10 +160,8 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when given an non existent user' do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- user_id: 0
- }
+ params: { user_id: 0 },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("Could not find the given user")
@@ -137,10 +169,8 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when no name parameter is received' do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- key_id: key.id
- }
+ params: { key_id: key.id },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("No token name specified")
@@ -148,11 +178,8 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when no scopes parameter is received' do
post api('/internal/personal_access_token'),
- params: {
- secret_token: secret_token,
- key_id: key.id,
- name: 'newtoken'
- }
+ params: { key_id: key.id, name: 'newtoken' },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("No token scopes specified")
@@ -161,12 +188,12 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when expires_at contains an invalid date' do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: ['api'],
expires_at: 'invalid-date'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq("Invalid token expiry date: 'invalid-date'")
@@ -175,11 +202,11 @@ RSpec.describe API::Internal::Base do
it 'returns an error message when it receives an invalid scope' do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: %w(read_api badscope read_repository)
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_falsey
expect(json_response['message']).to match(/\AInvalid scope: 'badscope'. Valid scopes are: /)
@@ -190,11 +217,11 @@ RSpec.describe API::Internal::Base do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: %w(read_api read_repository)
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
@@ -207,12 +234,12 @@ RSpec.describe API::Internal::Base do
post api('/internal/personal_access_token'),
params: {
- secret_token: secret_token,
- key_id: key.id,
+ key_id: key.id,
name: 'newtoken',
scopes: %w(read_api read_repository),
expires_at: '9001-11-17'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
@@ -309,7 +336,7 @@ RSpec.describe API::Internal::Base do
describe "GET /internal/discover" do
it "finds a user by key id" do
- get(api("/internal/discover"), params: { key_id: key.id, secret_token: secret_token })
+ get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -317,7 +344,7 @@ RSpec.describe API::Internal::Base do
end
it "finds a user by username" do
- get(api("/internal/discover"), params: { username: user.username, secret_token: secret_token })
+ get(api("/internal/discover"), params: { username: user.username }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -325,7 +352,7 @@ RSpec.describe API::Internal::Base do
end
it 'responds successfully when a user is not found' do
- get(api('/internal/discover'), params: { username: 'noone', secret_token: secret_token })
+ get(api('/internal/discover'), params: { username: 'noone' }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -333,7 +360,7 @@ RSpec.describe API::Internal::Base do
end
it 'response successfully when passing invalid params' do
- get(api('/internal/discover'), params: { nothing: 'to find a user', secret_token: secret_token })
+ get(api('/internal/discover'), params: { nothing: 'to find a user' }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
@@ -344,7 +371,7 @@ RSpec.describe API::Internal::Base do
describe "GET /internal/authorized_keys" do
context "using an existing key" do
it "finds the key" do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(key.id)
@@ -352,7 +379,7 @@ RSpec.describe API::Internal::Base do
end
it 'exposes the comment of the key as a simple identifier of username + hostname' do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})")
@@ -360,13 +387,13 @@ RSpec.describe API::Internal::Base do
end
it "returns 404 with a partial key" do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3], secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3] }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 404 with an not valid base64 string" do
- get(api('/internal/authorized_keys'), params: { key: "whatever!", secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: "whatever!" }, headers: gitlab_shell_internal_api_request_header)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -609,9 +636,9 @@ RSpec.describe API::Internal::Base do
project: full_path_for(project),
gl_repository: gl_repository_for(project),
action: 'git-upload-pack',
- secret_token: secret_token,
protocol: 'ssh'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
)
end
end
@@ -994,9 +1021,9 @@ RSpec.describe API::Internal::Base do
key_id: key.id,
project: 'project/does-not-exist.git',
action: 'git-upload-pack',
- secret_token: secret_token,
protocol: 'ssh'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1170,9 +1197,9 @@ RSpec.describe API::Internal::Base do
key_id: key.id,
project: project.full_path,
gl_repository: gl_repository,
- secret_token: secret_token,
protocol: 'ssh'
- })
+ }, headers: gitlab_shell_internal_api_request_header
+ )
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -1285,7 +1312,6 @@ RSpec.describe API::Internal::Base do
let(:valid_params) do
{
gl_repository: gl_repository,
- secret_token: secret_token,
identifier: identifier,
changes: changes,
push_options: push_options
@@ -1296,7 +1322,7 @@ RSpec.describe API::Internal::Base do
"#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
end
- subject { post api('/internal/post_receive'), params: valid_params }
+ subject { post api('/internal/post_receive'), params: valid_params, headers: gitlab_shell_internal_api_request_header }
before do
project.add_developer(user)
@@ -1397,7 +1423,7 @@ RSpec.describe API::Internal::Base do
describe 'POST /internal/pre_receive' do
let(:valid_params) do
- { gl_repository: gl_repository, secret_token: secret_token }
+ { gl_repository: gl_repository }
end
it 'decreases the reference counter and returns the result' do
@@ -1405,7 +1431,7 @@ RSpec.describe API::Internal::Base do
.and_return(reference_counter)
expect(reference_counter).to receive(:increase).and_return(true)
- post api("/internal/pre_receive"), params: valid_params
+ post api("/internal/pre_receive"), params: valid_params, headers: gitlab_shell_internal_api_request_header
expect(json_response['reference_counter_increased']).to be(true)
end
@@ -1420,10 +1446,8 @@ RSpec.describe API::Internal::Base do
subject do
post api('/internal/two_factor_config'),
- params: {
- secret_token: secret_token,
- key_id: key_id
- }
+ params: { key_id: key_id },
+ headers: gitlab_shell_internal_api_request_header
end
it_behaves_like 'actor key validations'
@@ -1478,27 +1502,6 @@ RSpec.describe API::Internal::Base do
end
end
- describe 'POST /internal/two_factor_otp_check' do
- let(:key_id) { key.id }
- let(:otp) { '123456' }
-
- subject do
- post api('/internal/two_factor_otp_check'),
- params: {
- secret_token: secret_token,
- key_id: key_id,
- otp_attempt: otp
- }
- end
-
- it 'is not available' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq 'Feature is not available'
- end
- end
-
describe 'POST /internal/two_factor_manual_otp_check' do
let(:key_id) { key.id }
let(:otp) { '123456' }
@@ -1509,7 +1512,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1530,7 +1534,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1551,7 +1556,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1571,7 +1577,8 @@ RSpec.describe API::Internal::Base do
secret_token: secret_token,
key_id: key_id,
otp_attempt: otp
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
end
it 'is not available' do
@@ -1584,32 +1591,24 @@ RSpec.describe API::Internal::Base do
def lfs_auth_project(project)
post(
api("/internal/lfs_authenticate"),
- params: {
- secret_token: secret_token,
- project: project.full_path
- }
+ params: { project: project.full_path },
+ headers: gitlab_shell_internal_api_request_header
)
end
def lfs_auth_key(key_id, project)
post(
api("/internal/lfs_authenticate"),
- params: {
- key_id: key_id,
- secret_token: secret_token,
- project: project.full_path
- }
+ params: { key_id: key_id, project: project.full_path },
+ headers: gitlab_shell_internal_api_request_header
)
end
def lfs_auth_user(user_id, project)
post(
api("/internal/lfs_authenticate"),
- params: {
- user_id: user_id,
- secret_token: secret_token,
- project: project.full_path
- }
+ params: { user_id: user_id, project: project.full_path },
+ headers: gitlab_shell_internal_api_request_header
)
end
end
diff --git a/spec/requests/api/internal/lfs_spec.rb b/spec/requests/api/internal/lfs_spec.rb
index 4739ec62992..9eb48db5bd5 100644
--- a/spec/requests/api/internal/lfs_spec.rb
+++ b/spec/requests/api/internal/lfs_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe API::Internal::Lfs do
+ include GitlabShellHelpers
include APIInternalBaseHelpers
let_it_be(:project) { create(:project) }
@@ -11,25 +12,23 @@ RSpec.describe API::Internal::Lfs do
let_it_be(:gl_repository) { "project-#{project.id}" }
let_it_be(:filename) { lfs_object.file.path }
- let(:secret_token) { Gitlab::Shell.secret_token }
-
describe 'GET /internal/lfs' do
let(:valid_params) do
- { oid: lfs_object.oid, gl_repository: gl_repository, secret_token: secret_token }
+ { oid: lfs_object.oid, gl_repository: gl_repository }
end
context 'with invalid auth' do
- let(:invalid_params) { valid_params.merge!(secret_token: 'invalid_tokne') }
-
it 'returns 401' do
- get api("/internal/lfs"), params: invalid_params
+ get api("/internal/lfs"),
+ params: valid_params,
+ headers: gitlab_shell_internal_api_request_header(issuer: 'gitlab-workhorse')
end
end
context 'with valid auth' do
context 'LFS in local storage' do
it 'sends the file' do
- get api("/internal/lfs"), params: valid_params
+ get api("/internal/lfs"), params: valid_params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('application/octet-stream')
@@ -39,7 +38,10 @@ RSpec.describe API::Internal::Lfs do
# https://www.rubydoc.info/github/rack/rack/master/Rack/Sendfile
it 'delegates sending to Web server' do
- get api("/internal/lfs"), params: valid_params, env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }
+ get api("/internal/lfs"),
+ params: valid_params,
+ env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' },
+ headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('application/octet-stream')
@@ -51,7 +53,7 @@ RSpec.describe API::Internal::Lfs do
it 'retuns 404 for unknown file' do
params = valid_params.merge(oid: SecureRandom.hex)
- get api("/internal/lfs"), params: params
+ get api("/internal/lfs"), params: params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -60,7 +62,7 @@ RSpec.describe API::Internal::Lfs do
other_lfs = create(:lfs_object, :with_file)
params = valid_params.merge(oid: other_lfs.oid)
- get api("/internal/lfs"), params: params
+ get api("/internal/lfs"), params: params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -70,7 +72,7 @@ RSpec.describe API::Internal::Lfs do
let!(:lfs_object2) { create(:lfs_object, :with_file) }
let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) }
let(:valid_params) do
- { oid: lfs_object2.oid, gl_repository: gl_repository, secret_token: secret_token }
+ { oid: lfs_object2.oid, gl_repository: gl_repository }
end
before do
@@ -79,7 +81,7 @@ RSpec.describe API::Internal::Lfs do
end
it 'notifies Workhorse to send the file' do
- get api("/internal/lfs"), params: valid_params
+ get api("/internal/lfs"), params: valid_params, headers: gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 3663a82891c..5c06214316b 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -465,10 +465,10 @@ RSpec.describe API::Issues do
context 'with archived projects' do
let_it_be(:archived_issue) do
- create(
- :issue, author: user, assignees: [user],
- project: create(:project, :public, :archived, creator_id: user.id, namespace: group)
- )
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: create(:project, :public, :archived, creator_id: user.id, namespace: group))
end
it 'returns only non archived projects issues' do
diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb
index 1270efdfd6f..f2019172a54 100644
--- a/spec/requests/api/markdown_snapshot_spec.rb
+++ b/spec/requests/api/markdown_snapshot_spec.rb
@@ -5,7 +5,5 @@ require 'spec_helper'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec.
RSpec.describe API::Markdown, 'Snapshot' do
- # noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil)
- glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__)
- include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir
+ include_context 'with API::Markdown Snapshot shared context'
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 1b378788b6a..d7cc6991ef4 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -2,6 +2,7 @@
require 'spec_helper'
RSpec.describe API::MavenPackages do
+ using RSpec::Parameterized::TableSyntax
include WorkhorseHelpers
include_context 'workhorse headers'
@@ -40,15 +41,15 @@ RSpec.describe API::MavenPackages do
project.add_developer(user)
end
- shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: %i[public]|
+ shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: { public: :redirect }|
context 'within a group' do
- visibilities.each do |visibility|
+ visibilities.each do |visibility, not_found_response|
context "that is #{visibility}" do
before do
group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
- it_behaves_like shared_example_name
+ it_behaves_like shared_example_name, not_found_response
end
end
end
@@ -60,20 +61,20 @@ RSpec.describe API::MavenPackages do
move_project_to_namespace(subgroup)
end
- visibilities.each do |visibility|
+ visibilities.each do |visibility, not_found_response|
context "that is #{visibility}" do
before do
subgroup.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
- it_behaves_like shared_example_name
+ it_behaves_like shared_example_name, not_found_response
end
end
end
end
- shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: %i[public]|
+ shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: { public: :redirect }|
it_behaves_like 'handling groups and subgroups for', shared_example_name, visibilities: visibilities
context 'within a user namespace' do
@@ -103,16 +104,6 @@ RSpec.describe API::MavenPackages do
end
end
- shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found|
- it 'rejects the request' do
- expect(::Packages::Maven::PackageFinder).not_to receive(:new)
-
- subject
-
- expect(response).to have_gitlab_http_status(expected_status)
- end
- end
-
shared_examples 'processing HEAD requests' do |instance_level: false|
subject { head api(url) }
@@ -162,7 +153,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
let(:path) { 'foo/bar/1.2.3' }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: instance_level ? :forbidden : :not_found
+ it_behaves_like 'returning response status', instance_level ? :forbidden : :redirect
end
end
end
@@ -238,12 +229,66 @@ RSpec.describe API::MavenPackages do
end
end
+ shared_examples 'forwarding package requests' do
+ context 'request forwarding' do
+ include_context 'dependency proxy helpers context'
+
+ subject { download_file(file_name: package_name) }
+
+ shared_examples 'redirecting the request' do
+ it_behaves_like 'returning response status', :redirect
+ end
+
+ shared_examples 'package not found' do
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ where(:forward, :package_in_project, :shared_examples_name) do
+ true | true | 'successfully returning the file'
+ true | false | 'redirecting the request'
+ false | true | 'successfully returning the file'
+ false | false | 'package not found'
+ end
+
+ with_them do
+ let(:package_name) { package_in_project ? package_file.file_name : 'foo' }
+
+ before do
+ allow_fetch_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward)
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+
+ context 'with maven_central_request_forwarding disabled' do
+ where(:forward, :package_in_project, :shared_examples_name) do
+ true | true | 'successfully returning the file'
+ true | false | 'package not found'
+ false | true | 'successfully returning the file'
+ false | false | 'package not found'
+ end
+
+ with_them do
+ let(:package_name) { package_in_project ? package_file.file_name : 'foo' }
+
+ before do
+ stub_feature_flags(maven_central_request_forwarding: false)
+ allow_fetch_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward)
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+ end
+ end
+
describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
it_behaves_like 'file download in FIPS mode'
@@ -258,7 +303,16 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
+ it_behaves_like 'returning response status', :forbidden
+ end
+
+ it 'returns not found when a package is not found' do
+ finder = double('finder', execute: nil)
+ expect(::Packages::Maven::PackageFinder).to receive(:new).and_return(finder)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -275,7 +329,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
-
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
it 'denies download when no private token' do
@@ -285,17 +339,16 @@ RSpec.describe API::MavenPackages do
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
+ it_behaves_like 'returning response status', :forbidden
end
end
- it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal]
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found }
end
context 'private project' do
@@ -307,7 +360,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
-
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
it 'denies download when not enough permissions' do
@@ -327,7 +380,6 @@ RSpec.describe API::MavenPackages do
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
it 'does not allow download by a unauthorized deploy token with same id as a user with access' do
@@ -350,11 +402,11 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
+ it_behaves_like 'returning response status', :forbidden
end
end
- it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal private]
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found, private: :not_found }
end
context 'project name is different from a package name' do
@@ -409,11 +461,14 @@ RSpec.describe API::MavenPackages do
group.add_developer(user)
end
+ it_behaves_like 'forwarding package requests'
+
context 'a public project' do
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file for a group' do
it_behaves_like 'tracking the file download event'
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
it_behaves_like 'file download in FIPS mode'
@@ -428,7 +483,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
@@ -443,29 +498,28 @@ RSpec.describe API::MavenPackages do
subject { download_file_with_token(file_name: package_file.file_name) }
- shared_examples 'getting a file for a group' do
+ shared_examples 'getting a file for a group' do |not_found_response|
it_behaves_like 'tracking the file download event'
-
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
- it 'denies download when no private token' do
+ it 'forwards download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(not_found_response)
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
- it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[internal public]
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :not_found, public: :redirect }
end
context 'private project' do
@@ -475,9 +529,9 @@ RSpec.describe API::MavenPackages do
subject { download_file_with_token(file_name: package_file.file_name) }
- shared_examples 'getting a file for a group' do
+ shared_examples 'getting a file for a group' do |not_found_response|
it_behaves_like 'tracking the file download event'
-
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
it 'denies download when not enough permissions' do
@@ -485,23 +539,22 @@ RSpec.describe API::MavenPackages do
subject
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:redirect)
end
it 'denies download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(not_found_response)
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
context 'with group deploy token' do
@@ -521,12 +574,12 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: group_deploy_token_headers) }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
end
- it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[private internal public]
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :not_found, internal: :not_found, public: :redirect }
context 'with a reporter from a subgroup accessing the root group' do
let_it_be(:root_group) { create(:group, :private) }
@@ -544,7 +597,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
end
@@ -640,12 +693,14 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'successfully returning the file'
it_behaves_like 'file download in FIPS mode'
- it 'returns sha1 of the file' do
- download_file(file_name: package_file.file_name + '.sha1')
+ %w[sha1 md5].each do |format|
+ it "returns #{format} of the file" do
+ download_file(file_name: package_file.file_name + ".#{format}")
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('text/plain')
- expect(response.body).to eq(package_file.file_sha1)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('text/plain')
+ expect(response.body).to eq(package_file.send("file_#{format}".to_sym))
+ end
end
context 'when the repository is disabled' do
@@ -664,7 +719,7 @@ RSpec.describe API::MavenPackages do
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
@@ -676,7 +731,7 @@ RSpec.describe API::MavenPackages do
subject { download_file_with_token(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
-
+ it_behaves_like 'bumping the package last downloaded at field'
it_behaves_like 'successfully returning the file'
it 'denies download when not enough permissions' do
@@ -694,16 +749,17 @@ RSpec.describe API::MavenPackages do
end
it_behaves_like 'downloads with a job token'
-
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'rejecting the request for non existing maven path'
+ it_behaves_like 'returning response status', :redirect
end
end
+ it_behaves_like 'forwarding package requests'
+
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
get api("/projects/#{project.id}/packages/maven/" \
"#{path}/#{file_name}"), params: params, headers: request_headers
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 2a03ae89389..9d153286d14 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe API::MergeRequests do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
@@ -1022,6 +1023,22 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'a non-cached MergeRequest api request', 1
end
+ context 'when the assignees change' do
+ before do
+ merge_request.assignees << create(:user)
+ end
+
+ it_behaves_like 'a non-cached MergeRequest api request', 1
+ end
+
+ context 'when the reviewers change' do
+ before do
+ merge_request.reviewers << create(:user)
+ end
+
+ it_behaves_like 'a non-cached MergeRequest api request', 1
+ end
+
context 'when another user requests' do
before do
sign_in(user2)
@@ -1120,6 +1137,44 @@ RSpec.describe API::MergeRequests do
end.not_to exceed_query_limit(control)
end
end
+
+ context 'when user is an inherited member from the group' do
+ let_it_be(:group) { create(:group) }
+
+ shared_examples 'user cannot view merge requests' do
+ it 'returns 403 forbidden' do
+ get api("/projects/#{group_project.id}/merge_requests", inherited_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'and user is a guest' do
+ let_it_be(:inherited_user) { create(:user) }
+
+ before_all do
+ group.add_guest(inherited_user)
+ end
+
+ context 'when project is public with private merge requests' do
+ let(:group_project) do
+ create(:project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED)
+ end
+
+ it_behaves_like 'user cannot view merge requests'
+ end
+
+ context 'when project is private' do
+ let(:group_project) { create(:project, :private, :repository, group: group) }
+
+ it_behaves_like 'user cannot view merge requests'
+ end
+ end
+ end
end
describe "GET /groups/:id/merge_requests" do
@@ -1528,7 +1583,6 @@ RSpec.describe API::MergeRequests do
expect(json_response.last['user']['name']).to eq(reviewer.name)
expect(json_response.last['user']['username']).to eq(reviewer.username)
expect(json_response.last['state']).to eq('unreviewed')
- expect(json_response.last['updated_state_by']).to be_nil
expect(json_response.last['created_at']).to be_present
end
@@ -2219,6 +2273,59 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:created)
end
end
+
+ context 'when user is an inherited member from the group' do
+ let_it_be(:group) { create(:group) }
+
+ shared_examples 'user cannot create merge requests' do
+ it 'returns 403 forbidden' do
+ post api("/projects/#{group_project.id}/merge_requests", inherited_user), params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'and user is a guest' do
+ let_it_be(:inherited_user) { create(:user) }
+ let_it_be(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: inherited_user.id
+ }
+ end
+
+ before_all do
+ group.add_guest(inherited_user)
+ end
+
+ context 'when project is public with private merge requests' do
+ let(:group_project) do
+ create(:project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ only_allow_merge_if_pipeline_succeeds: false)
+ end
+
+ it_behaves_like 'user cannot create merge requests'
+ end
+
+ context 'when project is private' do
+ let(:group_project) do
+ create(:project,
+ :private,
+ :repository,
+ group: group,
+ only_allow_merge_if_pipeline_succeeds: false)
+ end
+
+ it_behaves_like 'user cannot create merge requests'
+ end
+ end
+ end
end
describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do
@@ -2247,6 +2354,16 @@ RSpec.describe API::MergeRequests do
expect(merge_request.notes.system.last.note).to include("assigned to #{user2.to_reference}")
end
+
+ it 'triggers webhooks', :sidekiq_inline do
+ hook = create(:project_hook, merge_requests_events: true, project: merge_request.project)
+
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when assignee_id=user2.id' do
@@ -3373,7 +3490,8 @@ RSpec.describe API::MergeRequests do
context 'when merge request branch does not allow force push' do
before do
- create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false)
+ create_params = { name: merge_request.source_branch, allow_force_push: false, merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] }
+ ProtectedBranches::CreateService.new(project, project.first_owner, create_params).execute
end
it 'returns 403' do
@@ -3413,6 +3531,71 @@ RSpec.describe API::MergeRequests do
end
end
+ describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do
+ before do
+ merge_request.approvals.create!(user: user2)
+ create(:project_member, :maintainer, user: bot, source: project)
+ end
+
+ context 'when reset_approvals can be performed' do
+ it 'clears approvals of the merge_request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(merge_request.approvals).to be_empty
+ end
+
+ it 'for users with bot role' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'for users with non-bot roles' do
+ let(:human_user) { create(:user) }
+
+ [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method|
+ it 'returns 401' do
+ project.send(role_method, human_user)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ context 'for bot-users from external namespaces' do
+ let_it_be(:external_bot) { create(:user, :project_bot) }
+
+ context 'external group bot-user' do
+ before do
+ create(:group_member, :maintainer, user: external_bot, source: create(:group))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'external project bot-user' do
+ before do
+ create(:project_member, :maintainer, user: external_bot, source: create(:project))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+ end
+ end
+
describe 'Time tracking' do
let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb
new file mode 100644
index 00000000000..4e7091a5b0f
--- /dev/null
+++ b/spec/requests/api/ml/mlflow_spec.rb
@@ -0,0 +1,366 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'mime/types'
+
+RSpec.describe API::Ml::Mlflow do
+ include SessionHelpers
+ include ApiHelpers
+ include HttpBasicAuthHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:experiment) do
+ create(:ml_experiments, user: project.creator, project: project)
+ end
+
+ let_it_be(:candidate) do
+ create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment)
+ end
+
+ let_it_be(:another_candidate) do
+ create(:ml_candidates,
+ experiment: create(:ml_experiments, project: create(:project)))
+ end
+
+ let(:current_user) { developer }
+ let(:ff_value) { true }
+ let(:scopes) { %w[read_api api] }
+ let(:headers) do
+ { 'Authorization' => "Bearer #{create(:personal_access_token, scopes: scopes, user: current_user).token}" }
+ end
+
+ let(:params) { {} }
+ let(:request) { get api(route), params: params, headers: headers }
+
+ before do
+ stub_feature_flags(ml_experiment_tracking: ff_value)
+
+ request
+ end
+
+ shared_examples 'Not Found' do |message|
+ it "is Not Found" do
+ expect(response).to have_gitlab_http_status(:not_found)
+
+ expect(json_response['message']).to eq(message) if message.present?
+ end
+ end
+
+ shared_examples 'Not Found - Resource Does Not Exist' do
+ it "is Resource Does Not Exist" do
+ expect(response).to have_gitlab_http_status(:not_found)
+
+ expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
+ end
+ end
+
+ shared_examples 'Requires api scope' do
+ context 'when user has access but token has wrong scope' do
+ let(:scopes) { %w[read_api] }
+
+ it { expect(response).to have_gitlab_http_status(:forbidden) }
+ end
+ end
+
+ shared_examples 'Requires read_api scope' do
+ context 'when user has access but token has wrong scope' do
+ let(:scopes) { %w[read_user] }
+
+ it { expect(response).to have_gitlab_http_status(:forbidden) }
+ end
+ end
+
+ shared_examples 'Bad Request' do |error_code = nil|
+ it "is Bad Request" do
+ expect(response).to have_gitlab_http_status(:bad_request)
+
+ expect(json_response).to include({ 'error_code' => error_code }) if error_code.present?
+ end
+ end
+
+ shared_examples 'shared error cases' do
+ context 'when not authenticated' do
+ let(:headers) { {} }
+
+ it "is Unauthorized" do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user does not have access' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'Not Found'
+ end
+
+ context 'when ff is disabled' do
+ let(:ff_value) { false }
+
+ it_behaves_like 'Not Found'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/get' do
+ let(:experiment_iid) { experiment.iid.to_s }
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" }
+
+ it 'returns the experiment' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('ml/get_experiment')
+ expect(json_response).to include({
+ 'experiment' => {
+ 'experiment_id' => experiment_iid,
+ 'name' => experiment.name,
+ 'lifecycle_stage' => 'active',
+ 'artifact_location' => 'not_implemented'
+ }
+ })
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and experiment does not exist' do
+ let(:experiment_iid) { non_existing_record_iid.to_s }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'and experiment_id is not passed' do
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get" }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires read_api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/experiments/get-by-name' do
+ let(:experiment_name) { experiment.name }
+ let(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}"
+ end
+
+ it 'returns the experiment' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('ml/get_experiment')
+ expect(json_response).to include({
+ 'experiment' => {
+ 'experiment_id' => experiment.iid.to_s,
+ 'name' => experiment_name,
+ 'lifecycle_stage' => 'active',
+ 'artifact_location' => 'not_implemented'
+ }
+ })
+ end
+
+ describe 'Error States' do
+ context 'when has access but experiment does not exist' do
+ let(:experiment_name) { "random_experiment" }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when has access but experiment_name is not passed' do
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name" }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires read_api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/experiments/create' do
+ let(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/create"
+ end
+
+ let(:params) { { name: 'new_experiment' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the experiment' do
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include('experiment_id' )
+ end
+
+ describe 'Error States' do
+ context 'when experiment name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ context 'when experiment name already exists' do
+ let(:existing_experiment) do
+ create(:ml_experiments, user: current_user, project: project)
+ end
+
+ let(:params) { { name: existing_experiment.name } }
+
+ it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS'
+ end
+
+ context 'when project does not exist' do
+ let(:route) { "/projects/#{non_existing_record_id}/ml/mflow/api/2.0/mlflow/experiments/create" }
+
+ it_behaves_like 'Not Found', '404 Project Not Found'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ end
+ end
+
+ describe 'Runs' do
+ describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/runs/create' do
+ let(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/create"
+ end
+
+ let(:params) { { experiment_id: experiment.iid.to_s, start_time: Time.now.to_i } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the run' do
+ expected_properties = {
+ 'experiment_id' => params[:experiment_id],
+ 'user_id' => current_user.id.to_s,
+ 'start_time' => params[:start_time],
+ 'artifact_uri' => 'not_implemented',
+ 'status' => "RUNNING",
+ 'lifecycle_stage' => "active"
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('ml/run')
+ expect(json_response['run']).to include('info' => hash_including(**expected_properties), 'data' => {})
+ end
+
+ describe 'Error States' do
+ context 'when experiment id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ context 'when experiment id does not exist' do
+ let(:params) { { experiment_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/runs/get' do
+ let_it_be(:route) do
+ "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/get"
+ end
+
+ let_it_be(:candidate) { create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment) }
+
+ let(:params) { { 'run_id' => candidate.iid } }
+
+ it 'gets the run' do
+ expected_properties = {
+ 'experiment_id' => candidate.experiment.iid.to_s,
+ 'user_id' => candidate.user.id.to_s,
+ 'start_time' => candidate.start_time,
+ 'artifact_uri' => 'not_implemented',
+ 'status' => "RUNNING",
+ 'lifecycle_stage' => "active"
+ }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response).to match_response_schema('ml/run')
+ expect(json_response['run']).to include('info' => hash_including(**expected_properties), 'data' => {})
+ end
+
+ describe 'Error States' do
+ context 'when run id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id does not exist' do
+ let(:params) { { run_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id exists but does not belong to project' do
+ let(:params) { { run_id: another_candidate.iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires read_api scope'
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/runs/update' do
+ let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/update" }
+ let(:params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: Time.now.to_i } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'updates the run' do
+ expected_properties = {
+ 'experiment_id' => candidate.experiment.iid.to_s,
+ 'user_id' => candidate.user.id.to_s,
+ 'start_time' => candidate.start_time,
+ 'end_time' => params[:end_time],
+ 'artifact_uri' => 'not_implemented',
+ 'status' => 'FAILED',
+ 'lifecycle_stage' => 'active'
+ }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response).to match_response_schema('ml/update_run')
+ expect(json_response).to include('run_info' => hash_including(**expected_properties))
+ end
+
+ describe 'Error States' do
+ context 'when run id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id does not exist' do
+ let(:params) { { run_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id exists but does not belong to project' do
+ let(:params) { { run_id: another_candidate.iid.to_s } }
+
+ it_behaves_like 'Not Found - Resource Does Not Exist'
+ end
+
+ context 'when run id exists but status in invalid' do
+ let(:params) { { run_id: candidate.iid.to_s, status: 'YOLO', end_time: Time.now.to_i } }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ context 'when run id exists but end_time is invalid' do
+ let(:params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: 's' } }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ end
+ end
+end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 09b87f41b82..ab39c29653f 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -285,6 +285,14 @@ RSpec.describe API::Namespaces do
end
context 'when authenticated' do
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :namespace_exists do
+ let(:current_user) { user }
+
+ def request
+ get api("/namespaces/#{namespace1.path}/exists", current_user)
+ end
+ end
+
it 'returns JSON indicating the namespace exists and a suggestion' do
get api("/namespaces/#{namespace1.path}/exists", user)
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 3bcffac2760..bdcd6e7278d 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
context 'with job token' do
@@ -70,12 +71,14 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
end
context 'a public project' do
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
diff --git a/spec/requests/api/personal_access_tokens/self_revocation_spec.rb b/spec/requests/api/personal_access_tokens/self_revocation_spec.rb
new file mode 100644
index 00000000000..f829b39cc1e
--- /dev/null
+++ b/spec/requests/api/personal_access_tokens/self_revocation_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::PersonalAccessTokens::SelfRevocation do
+ let_it_be(:current_user) { create(:user) }
+
+ describe 'DELETE /personal_access_tokens/self' do
+ let(:path) { '/personal_access_tokens/self' }
+ let(:token) { create(:personal_access_token, user: current_user) }
+
+ subject(:delete_token) { delete api(path, personal_access_token: token) }
+
+ shared_examples 'revoking token succeeds' do
+ it 'revokes token' do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(token.reload).to be_revoked
+ end
+ end
+
+ shared_examples 'revoking token denied' do |status|
+ it 'cannot revoke token' do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ context 'when current_user is an administrator', :enable_admin_mode do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'revoking token succeeds'
+
+ context 'with impersonated token' do
+ let(:token) { create(:personal_access_token, :impersonation, user: current_user) }
+
+ it_behaves_like 'revoking token succeeds'
+ end
+ end
+
+ context 'when current_user is not an administrator' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'revoking token succeeds'
+
+ context 'with impersonated token' do
+ let(:token) { create(:personal_access_token, :impersonation, user: current_user) }
+
+ it_behaves_like 'revoking token denied', :bad_request
+ end
+
+ context 'with already revoked token' do
+ let(:token) { create(:personal_access_token, :revoked, user: current_user) }
+
+ it_behaves_like 'revoking token denied', :unauthorized
+ end
+ end
+
+ Gitlab::Auth.all_available_scopes.each do |scope|
+ context "with a '#{scope}' scoped token" do
+ let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) }
+
+ it_behaves_like 'revoking token succeeds'
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 8d8998cfdd6..37b5a594f2a 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -75,6 +75,7 @@ RSpec.describe API::PersonalAccessTokens do
describe 'GET /personal_access_tokens/:id' do
let_it_be(:user_token) { create(:personal_access_token, user: current_user) }
+ let_it_be(:user_read_only_token) { create(:personal_access_token, scopes: ['read_repository'], user: current_user) }
let_it_be(:user_token_path) { "/personal_access_tokens/#{user_token.id}" }
let_it_be(:invalid_path) { "/personal_access_tokens/#{non_existing_record_id}" }
@@ -125,53 +126,11 @@ RSpec.describe API::PersonalAccessTokens do
expect(response).to have_gitlab_http_status(:unauthorized)
end
- end
- end
-
- describe 'DELETE /personal_access_tokens/self' do
- let(:path) { '/personal_access_tokens/self' }
- let(:token) { create(:personal_access_token, user: current_user) }
-
- subject { delete api(path, current_user, personal_access_token: token) }
-
- shared_examples 'revoking token succeeds' do
- it 'revokes token' do
- subject
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(token.reload).to be_revoked
- end
- end
- shared_examples 'revoking token denied' do |status|
- it 'cannot revoke token' do
- subject
+ it 'fails to return own PAT by id with read_repository token' do
+ get api(user_token_path, current_user, personal_access_token: user_read_only_token)
- expect(response).to have_gitlab_http_status(status)
- end
- end
-
- context 'when current_user is an administrator', :enable_admin_mode do
- let(:current_user) { create(:admin) }
-
- it_behaves_like 'revoking token succeeds'
- end
-
- context 'when current_user is not an administrator' do
- let(:current_user) { create(:user) }
-
- it_behaves_like 'revoking token succeeds'
-
- context 'with impersonated token' do
- let(:token) { create(:personal_access_token, :impersonation, user: current_user) }
-
- it_behaves_like 'revoking token denied', :bad_request
- end
-
- context 'with already revoked token' do
- let(:token) { create(:personal_access_token, :revoked, user: current_user) }
-
- it_behaves_like 'revoking token denied', :unauthorized
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -183,6 +142,9 @@ RSpec.describe API::PersonalAccessTokens do
let_it_be(:admin_user) { create(:admin) }
let_it_be(:admin_token) { create(:personal_access_token, user: admin_user) }
let_it_be(:admin_path) { "/personal_access_tokens/#{admin_token.id}" }
+ let_it_be(:admin_read_only_token) do
+ create(:personal_access_token, scopes: ['read_repository'], user: admin_user)
+ end
it 'revokes a different users token' do
delete api(path, admin_user)
@@ -196,6 +158,12 @@ RSpec.describe API::PersonalAccessTokens do
expect(response).to have_gitlab_http_status(:no_content)
end
+
+ it 'fails to revoke a different user token using a readonly scope' do
+ delete api(path, personal_access_token: admin_read_only_token)
+
+ expect(token1.reload.revoked?).to be false
+ end
end
context 'when current_user is not an administrator' do
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 670035187cb..1335fa02aaf 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -154,11 +154,13 @@ project_setting:
- project_id
- push_rule_id
- show_default_award_emojis
+ - show_diff_preview_in_email
- updated_at
- cve_id_request_enabled
- mr_default_target_self
- target_platforms
- selective_code_owner_removals
+ - show_diff_preview_in_email
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index afe5a7d4a21..401db766589 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
it 'executes a limited number of queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
- expect(control_count).to be <= 109
+ expect(control_count).to be <= 110
end
it 'schedules an import using a namespace' do
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 7a05da8e13f..00d295b3490 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe API::ProjectPackages do
let_it_be(:project) { create(:project, :public) }
let(:user) { create(:user) }
- let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") }
+ let!(:package1) { create(:npm_package, :last_downloaded_at, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") }
let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" }
let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') }
let!(:another_package) { create(:npm_package) }
@@ -272,6 +272,17 @@ RSpec.describe API::ProjectPackages do
it_behaves_like 'returns package', :project, :no_type
it_behaves_like 'returns package', :project, :guest
end
+
+ context 'with a package without last_downloaded_at' do
+ let(:package_url) { "/projects/#{project.id}/packages/#{package2.id}" }
+
+ it 'returns 200 and the package information' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema(single_package_schema)
+ end
+ end
end
context 'project is private' do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 72519ed1683..6e2dd6e76a9 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -256,6 +256,7 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+ stub_feature_flags(allow_possible_spam: false)
project.add_developer(user)
end
@@ -311,6 +312,8 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 94688833d88..7ad1ce0ede9 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -48,6 +48,7 @@ end
RSpec.describe API::Projects do
include ProjectForksHelper
+ include WorkhorseHelpers
include StubRequests
let_it_be(:user) { create(:user) }
@@ -1249,9 +1250,10 @@ RSpec.describe API::Projects do
stub_application_setting(import_sources: nil)
endpoint_url = "#{url}/info/refs?service=git-upload-pack"
- stub_full_request(endpoint_url, method: :get).to_return({ status: 200,
- body: '001e# service=git-upload-pack',
- headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
+ stub_full_request(endpoint_url, method: :get).to_return(
+ { status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
expect { post api('/projects', user), params: project_params }
@@ -1348,7 +1350,12 @@ RSpec.describe API::Projects do
it 'uploads avatar for project a project' do
project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
- post api('/projects', user), params: project
+ workhorse_form_with_file(
+ api('/projects', user),
+ method: :post,
+ file_key: :avatar,
+ params: project
+ )
project_id = json_response['id']
expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
@@ -1924,8 +1931,6 @@ RSpec.describe API::Projects do
end
describe "POST /projects/:id/uploads/authorize" do
- include WorkhorseHelpers
-
let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
context 'with authorized user' do
@@ -3583,18 +3588,77 @@ RSpec.describe API::Projects do
end
end
- it 'updates avatar' do
- project_param = {
- avatar: fixture_file_upload('spec/fixtures/banana_sample.gif',
- 'image/gif')
- }
+ context 'with changes to the avatar' do
+ let_it_be(:avatar_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ let_it_be(:alternate_avatar_file) { fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png') }
+ let_it_be(:project_with_avatar, reload: true) do
+ create(:project,
+ :private,
+ :repository,
+ name: 'project-with-avatar',
+ creator_id: user.id,
+ namespace: user.namespace,
+ avatar: avatar_file)
+ end
- put api("/projects/#{project3.id}", user), params: project_param
+ it 'uploads avatar to project without an avatar' do
+ workhorse_form_with_file(
+ api("/projects/#{project3.id}", user),
+ method: :put,
+ file_key: :avatar,
+ params: { avatar: avatar_file }
+ )
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
- '-/system/project/avatar/'\
- "#{project3.id}/banana_sample.gif")
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project3.id}/banana_sample.gif")
+ end
+ end
+
+ it 'uploads and changes avatar to project with an avatar' do
+ workhorse_form_with_file(
+ api("/projects/#{project_with_avatar.id}", user),
+ method: :put,
+ file_key: :avatar,
+ params: { avatar: alternate_avatar_file }
+ )
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project_with_avatar.id}/rails_sample.png")
+ end
+ end
+
+ it 'uploads and changes avatar to project among other changes' do
+ workhorse_form_with_file(
+ api("/projects/#{project_with_avatar.id}", user),
+ method: :put,
+ file_key: :avatar,
+ params: { description: 'changed description', avatar: avatar_file }
+ )
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['description']).to eq('changed description')
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
+ '-/system/project/avatar/'\
+ "#{project_with_avatar.id}/banana_sample.gif")
+ end
+ end
+
+ it 'removes avatar from project with an avatar' do
+ put api("/projects/#{project_with_avatar.id}", user), params: { avatar: '' }
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['avatar_url']).to be_nil
+ expect(project_with_avatar.reload.avatar_url).to be_nil
+ end
+ end
end
it 'updates auto_devops_deploy_strategy' do
@@ -4645,6 +4709,100 @@ RSpec.describe API::Projects do
end
end
+ describe 'GET /projects/:id/transfer_locations' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_group) { create(:group) }
+ let_it_be(:project) { create(:project, group: source_group) }
+
+ let(:params) { {} }
+
+ subject(:request) do
+ get api("/projects/#{project.id}/transfer_locations", user), params: params
+ end
+
+ context 'when the user has rights to transfer the project' do
+ let_it_be(:guest_group) { create(:group) }
+ let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') }
+ let_it_be(:owner_group) { create(:group, name: 'owner group', path: 'owner-group') }
+
+ before do
+ source_group.add_owner(user)
+ guest_group.add_guest(user)
+ maintainer_group.add_maintainer(user)
+ owner_group.add_owner(user)
+ end
+
+ it 'returns 200' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ end
+
+ it 'includes groups where the user has permissions to transfer a project to' do
+ request
+
+ expect(project_ids_from_response).to include(maintainer_group.id, owner_group.id)
+ end
+
+ it 'does not include groups where the user doesn not have permissions to transfer a project' do
+ request
+
+ expect(project_ids_from_response).not_to include(guest_group.id)
+ end
+
+ context 'with search' do
+ let(:params) { { search: 'maintainer' } }
+
+ it 'includes groups where the user has permissions to transfer a project to' do
+ request
+
+ expect(project_ids_from_response).to contain_exactly(maintainer_group.id)
+ end
+ end
+
+ context 'group shares' do
+ let_it_be(:shared_to_owner_group) { create(:group) }
+ let_it_be(:shared_to_guest_group) { create(:group) }
+
+ before do
+ create(:group_group_link, :owner,
+ shared_with_group: owner_group,
+ shared_group: shared_to_owner_group
+ )
+
+ create(:group_group_link, :guest,
+ shared_with_group: guest_group,
+ shared_group: shared_to_guest_group
+ )
+ end
+
+ it 'only includes groups arising from group shares where the user has permission to transfer a project to' do
+ request
+
+ expect(project_ids_from_response).to include(shared_to_owner_group.id)
+ expect(project_ids_from_response).not_to include(shared_to_guest_group.id)
+ end
+ end
+
+ def project_ids_from_response
+ json_response.map { |project| project['id'] }
+ end
+ end
+
+ context 'when the user does not have permissions to transfer the project' do
+ before do
+ source_group.add_developer(user)
+ end
+
+ it 'returns 403' do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
describe 'GET /projects/:id/storage' do
context 'when unauthenticated' do
it 'does not return project storage data' do
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 1d9e3a6c887..754b77af60e 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -573,6 +573,224 @@ RSpec.describe API::Releases do
end
end
+ describe 'GET /projects/:id/releases/:tag_name/downloads/*file_path' do
+ let!(:release) { create(:release, project: project, tag: 'v0.1', author: maintainer) }
+ let!(:link) { create(:release_link, release: release, url: "#{url}#{filepath}", filepath: filepath) }
+ let(:filepath) { '/bin/bigfile.exe' }
+ let(:url) { 'https://google.com/-/jobs/140463678/artifacts/download' }
+
+ context 'with an invalid release tag' do
+ it 'returns 404 for maintater' do
+ get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
+
+ it 'returns project not found for no user' do
+ get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns forbidden for guest' do
+ get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'with a valid release tag' do
+ context 'when filepath is provided' do
+ context 'when filepath exists' do
+ it 'redirects to the file download URL' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", maintainer)
+
+ expect(response).to redirect_to("#{url}#{filepath}")
+ end
+
+ it 'redirects to the file download URL when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}"), params: { job_token: job.token }
+
+ expect(response).to redirect_to("#{url}#{filepath}")
+ end
+
+ context 'when user is a guest' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest)
+
+ expect(response).to redirect_to("#{url}#{filepath}")
+ end
+ end
+ end
+ end
+
+ context 'when filepath does not exists' do
+ it 'returns 404 for maintater' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+
+ it 'returns project not found for no user' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns forbidden for guest' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'when filepath is not provided' do
+ it 'returns 404 for maintater' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns project not found for no user' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns forbidden for guest' do
+ get api("/projects/#{project.id}/releases/v0.1/downloads", guest)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/releases/permalink/latest' do
+ context 'when there is no release' do
+ it 'returns not found' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns not found when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when there are more than one release' do
+ let!(:release_a) do
+ create(:release,
+ project: project,
+ tag: 'v0.1',
+ author: maintainer,
+ description: 'This is v0.1',
+ released_at: 3.days.ago)
+ end
+
+ let!(:release_b) do
+ create(:release,
+ project: project,
+ tag: 'v0.2',
+ author: maintainer,
+ description: 'This is v0.2',
+ released_at: 2.days.ago)
+ end
+
+ it 'redirects to the latest release tag' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer)
+
+ uri = URI(response.header["Location"])
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ end
+
+ it 'redirects to the latest release tag when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token }
+
+ uri = URI(response.header["Location"])
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ end
+
+ context 'when there are query parameters present' do
+ it 'includes the query params on the redirection' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { include_html_description: true, other_param: "aaa" }
+
+ uri = URI(response.header["Location"])
+ query_params = Rack::Utils.parse_nested_query(uri.query)
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ expect(query_params).to include({
+ "include_html_description" => "true",
+ "other_param" => "aaa"
+ })
+ end
+
+ it 'discards the `order_by` query param' do
+ get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { order_by: 'something', other_param: "aaa" }
+
+ uri = URI(response.header["Location"])
+ query_params = Rack::Utils.parse_nested_query(uri.query)
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
+ expect(query_params).to include({
+ "other_param" => "aaa"
+ })
+ expect(query_params).not_to include({
+ "order_by" => "something"
+ })
+ end
+ end
+
+ context 'when downloading a release asset' do
+ it 'redirects to the right endpoint keeping the suffix_path' do
+ get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/example.exe", maintainer)
+
+ uri = URI(response.header["Location"])
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}/downloads/bin/example.exe")
+ end
+
+ it 'returns error when there is path traversal in suffix path' do
+ get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/../../../../../../../password.txt", maintainer)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+
+ expect(json_response['error']).to eq('suffix_path should be a valid file path')
+ end
+ end
+ end
+ end
+
describe 'POST /projects/:id/releases' do
let(:params) do
{
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 369a8c1b0ab..d9a12e7e148 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -243,27 +243,65 @@ RSpec.describe API::ResourceAccessTokens do
end
context "when the user has valid permissions" do
- it "deletes the #{source_type} access token from the #{source_type}" do
- delete_token
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
- expect(response).to have_gitlab_http_status(:no_content)
- expect(User.exists?(project_bot.id)).to be_falsy
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(
+ Users::GhostUserMigration.where(user: project_bot,
+ initiator_user: user)
+ ).to be_exists
+ end
- context "when using #{source_type} access token to DELETE other #{source_type} access token" do
- let_it_be(:other_project_bot) { create(:user, :project_bot) }
- let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
- let_it_be(:token_id) { other_token.id }
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
+ let_it_be(:other_project_bot) { create(:user, :project_bot) }
+ let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
+ let_it_be(:token_id) { other_token.id }
+
+ before do
+ resource.add_maintainer(other_project_bot)
+ end
+
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(
+ Users::GhostUserMigration.where(user: other_project_bot,
+ initiator_user: user)
+ ).to be_exists
+ end
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
before do
- resource.add_maintainer(other_project_bot)
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
end
it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
- expect(User.exists?(other_project_bot.id)).to be_falsy
+ expect(User.exists?(project_bot.id)).to be_falsy
+ end
+
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
+ let_it_be(:other_project_bot) { create(:user, :project_bot) }
+ let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
+ let_it_be(:token_id) { other_token.id }
+
+ before do
+ resource.add_maintainer(other_project_bot)
+ end
+
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(User.exists?(other_project_bot.id)).to be_falsy
+ end
end
end
diff --git a/spec/requests/api/resource_state_events_spec.rb b/spec/requests/api/resource_state_events_spec.rb
index 46ca9874395..5f756bc6c63 100644
--- a/spec/requests/api/resource_state_events_spec.rb
+++ b/spec/requests/api/resource_state_events_spec.rb
@@ -6,87 +6,8 @@ RSpec.describe API::ResourceStateEvents do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) }
- before_all do
- project.add_developer(user)
- end
-
- shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name|
- describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do
- let!(:event) { create_event }
-
- it "returns an array of resource state events" do
- url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events"
- get api(url, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(event.id)
- expect(json_response.first['state']).to eq(event.state.to_s)
- end
-
- it "returns a 404 error when eventable id not found" do
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns 404 when not authorized" do
- parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- private_user = create(:user)
-
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do
- let!(:event) { create_event }
-
- it "returns a resource state event by id" do
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['id']).to eq(event.id)
- expect(json_response['state']).to eq(event.state.to_s)
- end
-
- it "returns 404 when not authorized" do
- parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- private_user = create(:user)
-
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns a 404 error if resource state event not found" do
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe 'pagination' do
- # https://gitlab.com/gitlab-org/gitlab/-/issues/220192
- it 'returns the second page' do
- create_event
- event2 = create_event
-
- get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq '2'
- expect(json_response.count).to eq(1)
- expect(json_response.first['id']).to eq(event2.id)
- end
- end
-
- def create_event(state: :opened)
- create(:resource_state_event, eventable.class.name.underscore => eventable, state: state)
- end
+ before do
+ parent.add_developer(user)
end
context 'when eventable is an Issue' do
diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb
new file mode 100644
index 00000000000..6a646c26fd2
--- /dev/null
+++ b/spec/requests/api/rpm_project_packages_spec.rb
@@ -0,0 +1,250 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::RpmProjectPackages do
+ include HttpBasicAuthHelpers
+ include WorkhorseHelpers
+
+ include_context 'workhorse headers'
+
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
+ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+ let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+ let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
+
+ let(:headers) { {} }
+ let(:package_name) { 'rpm-package.0-1.x86_64.rpm' }
+ let(:package_file_id) { 1 }
+
+ shared_examples 'rejects rpm packages access' do |status|
+ it_behaves_like 'returning response status', status
+
+ if status == :unauthorized
+ it 'has the correct response header' do
+ subject
+
+ expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
+ end
+ end
+ end
+
+ shared_examples 'process rpm packages upload/download' do |status|
+ it_behaves_like 'returning response status', status
+ end
+
+ shared_examples 'a deploy token for RPM requests' do
+ context 'with deploy token headers' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
+
+ context 'when token is valid' do
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'when token is invalid' do
+ let(:headers) { basic_auth_header(deploy_token.username, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+ end
+
+ shared_examples 'a job token for RPM requests' do
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ project.add_developer(user)
+ end
+
+ context 'with valid token' do
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'with invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'with invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+ end
+
+ shared_examples 'a user token for RPM requests' do
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden
+ 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :guest | false | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized
+ 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
+ project.send("add_#{user_role}", user) if member && user_role != :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:expected_status]
+ end
+ end
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
+ let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" }
+
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'a job token for RPM requests'
+ it_behaves_like 'a deploy token for RPM requests'
+ it_behaves_like 'a user token for RPM requests'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
+ let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
+
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'a job token for RPM requests'
+ it_behaves_like 'a deploy token for RPM requests'
+ it_behaves_like 'a user token for RPM requests'
+ end
+
+ describe 'POST /api/v4/projects/:project_id/packages/rpm' do
+ let(:url) { "/projects/#{project.id}/packages/rpm" }
+ let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
+
+ subject { post api(url), params: { file: file_upload }, headers: headers }
+
+ context 'with user token' do
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found
+ 'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found
+ 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
+ project.send("add_#{user_role}", user) if member && user_role != :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:expected_status]
+ end
+ end
+
+ context 'when user can upload file' do
+ before do
+ project.add_developer(user)
+ end
+
+ let(:headers) { basic_auth_header(user.username, personal_access_token.token).merge(workhorse_headers) }
+
+ context 'when file size too large' do
+ before do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.rpm_max_file_size + 1)
+ end
+ end
+
+ it 'returns an error' do
+ upload_file(params: { file: file_upload }, request_headers: headers)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to match(/File is too large/)
+ end
+ end
+ end
+
+ def upload_file(params: {}, request_headers: headers)
+ url = "/projects/#{project.id}/packages/rpm"
+ workhorse_finalize(
+ api(url),
+ method: :post,
+ file_key: :file,
+ params: params,
+ headers: request_headers,
+ send_rewritten_field: true
+ )
+ end
+ end
+
+ it_behaves_like 'a deploy token for RPM requests'
+ it_behaves_like 'a job token for RPM requests'
+ end
+
+ describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do
+ let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") }
+
+ subject { post(url, headers: headers) }
+
+ it_behaves_like 'returning response status', :not_found
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(rpm_packages: false)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'when package feature is disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 6034d26f1d2..05f38aff6ab 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe API::Search do
let_it_be(:repo_project) { create(:project, :public, :repository, group: group) }
before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
end
@@ -351,6 +352,43 @@ RSpec.describe API::Search do
end
end
+ it 'increments the custom search sli apdex' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with(
+ elapsed: a_kind_of(Numeric),
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
+ end
+
+ it 'increments the custom search sli error rate with error false if no error occurred' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
+ error: false,
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
+ end
+
+ it 'increments the custom search sli error rate with error true if an error occurred' do
+ allow_next_instance_of(SearchService) do |service|
+ allow(service).to receive(:search_results).and_raise(ActiveRecord::QueryCanceled)
+ end
+
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
+ error: true,
+ search_scope: 'issues',
+ search_type: 'basic',
+ search_level: 'global'
+ )
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
+ end
+
it 'sets global search information for logging' do
expect(Gitlab::Instrumentation::GlobalSearchApi).to receive(:set_information).with(
type: 'basic',
@@ -618,7 +656,7 @@ RSpec.describe API::Search do
context 'when requesting basic search' do
it 'passes the parameter to search service' do
- expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true'))
+ expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true')).twice
get api(endpoint, user), params: { scope: 'issues', search: 'awesome', basic_search: 'true' }
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 6f0d5827a80..315c76c8ac3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -56,6 +56,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['project_runner_token_expiration_interval']).to be_nil
expect(json_response['max_export_size']).to eq(0)
expect(json_response['pipeline_limit_per_project_user_sha']).to eq(0)
+ expect(json_response['delete_inactive_projects']).to be(false)
+ expect(json_response['inactive_projects_delete_after_months']).to eq(2)
+ expect(json_response['inactive_projects_min_size_mb']).to eq(0)
+ expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(1)
end
end
@@ -148,7 +152,11 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
user_deactivation_emails_enabled: false,
admin_mode: true,
suggest_pipeline_enabled: false,
- users_get_by_id_limit: 456
+ users_get_by_id_limit: 456,
+ delete_inactive_projects: true,
+ inactive_projects_delete_after_months: 24,
+ inactive_projects_min_size_mb: 10,
+ inactive_projects_send_warning_email_after_months: 12
}
expect(response).to have_gitlab_http_status(:ok)
@@ -205,6 +213,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['user_deactivation_emails_enabled']).to be(false)
expect(json_response['suggest_pipeline_enabled']).to be(false)
expect(json_response['users_get_by_id_limit']).to eq(456)
+ expect(json_response['delete_inactive_projects']).to be(true)
+ expect(json_response['inactive_projects_delete_after_months']).to eq(24)
+ expect(json_response['inactive_projects_min_size_mb']).to eq(10)
+ expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(12)
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 0dd6e484e8d..031bcb612f4 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -340,6 +340,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
@@ -405,6 +406,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
+ stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb
index 7f53d379af5..2393a268693 100644
--- a/spec/requests/api/suggestions_spec.rb
+++ b/spec/requests/api/suggestions_spec.rb
@@ -34,15 +34,14 @@ RSpec.describe API::Suggestions do
end
let(:diff_note2) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position2,
- project: project)
+ create(:diff_note_on_merge_request, noteable: merge_request, position: position2, project: project)
end
let(:suggestion) do
- create(:suggestion, note: diff_note,
- from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n",
- to_content: " raise RuntimeError, 'Explosion'\n # explosion?")
+ create(:suggestion,
+ note: diff_note,
+ from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n",
+ to_content: " raise RuntimeError, 'Explosion'\n # explosion?")
end
let(:unappliable_suggestion) do
@@ -119,8 +118,8 @@ RSpec.describe API::Suggestions do
describe "PUT /suggestions/batch_apply" do
let(:suggestion2) do
create(:suggestion, note: diff_note2,
- from_content: " \"PWD\" => path\n",
- to_content: " *** FOO ***\n")
+ from_content: " \"PWD\" => path\n",
+ to_content: " *** FOO ***\n")
end
let(:url) { "/suggestions/batch_apply" }
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index e81e9e0bf2f..b62fbaead6f 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -17,10 +17,6 @@ RSpec.describe API::Tags do
end
describe 'GET /projects/:id/repository/tags', :use_clean_rails_memory_store_caching do
- before do
- stub_feature_flags(tag_list_keyset_pagination: false)
- end
-
let(:route) { "/projects/#{project_id}/repository/tags" }
context 'sorting' do
@@ -59,6 +55,18 @@ RSpec.describe API::Tags do
expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
end
+
+ it 'sorts by version in ascending order when requested' do
+ repository = project.repository
+ repository.add_tag(user, 'v1.2.0', repository.commit.id)
+ repository.add_tag(user, 'v1.10.0', repository.commit.id)
+
+ get api("#{route}?order_by=version&sort=asc", current_user)
+
+ ordered_by_version = VersionSorter.sort(project.repository.tags.map { |tag| tag.name })
+
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_version)
+ end
end
context 'searching' do
@@ -154,50 +162,44 @@ RSpec.describe API::Tags do
end
end
- context 'with keyset pagination on', :aggregate_errors do
- before do
- stub_feature_flags(tag_list_keyset_pagination: true)
- end
-
- context 'with keyset pagination option' do
- let(:base_params) { { pagination: 'keyset' } }
+ context 'with keyset pagination option', :aggregate_errors do
+ let(:base_params) { { pagination: 'keyset' } }
- context 'with gitaly pagination params' do
- context 'with high limit' do
- let(:params) { base_params.merge(per_page: 100) }
+ context 'with gitaly pagination params' do
+ context 'with high limit' do
+ let(:params) { base_params.merge(per_page: 100) }
- it 'returns all repository tags' do
- get api(route, user), params: params
+ it 'returns all repository tags' do
+ get api(route, user), params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response.headers).not_to include('Link')
- tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(project.repository.tag_names)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).not_to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(project.repository.tag_names)
end
+ end
- context 'with low limit' do
- let(:params) { base_params.merge(per_page: 2) }
+ context 'with low limit' do
+ let(:params) { base_params.merge(per_page: 2) }
- it 'returns limited repository tags' do
- get api(route, user), params: params
+ it 'returns limited repository tags' do
+ get api(route, user), params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/tags')
- expect(response.headers).to include('Link')
- tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
end
+ end
- context 'with missing page token' do
- let(:params) { base_params.merge(page_token: 'unknown') }
+ context 'with missing page token' do
+ let(:params) { base_params.merge(page_token: 'unknown') }
- it_behaves_like '422 response' do
- let(:request) { get api(route, user), params: params }
- let(:message) { 'Invalid page token: refs/tags/unknown' }
- end
+ it_behaves_like '422 response' do
+ let(:request) { get api(route, user), params: params }
+ let(:message) { 'Invalid page token: refs/tags/unknown' }
end
end
end
diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb
index 72221e3fb6a..1ad6f876fab 100644
--- a/spec/requests/api/topics_spec.rb
+++ b/spec/requests/api/topics_spec.rb
@@ -317,4 +317,66 @@ RSpec.describe API::Topics do
end
end
end
+
+ describe 'POST /topics/merge', :aggregate_failures do
+ context 'as administrator' do
+ let_it_be(:api_url) { api('/topics/merge', admin) }
+
+ it 'merge topics' do
+ post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect { topic_2.reload }.not_to raise_error
+ expect { topic_3.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(json_response['id']).to eq(topic_2.id)
+ expect(json_response['total_projects_count']).to eq(topic_2.total_projects_count)
+ end
+
+ it 'returns 404 for non existing source topic id' do
+ post api_url, params: { source_topic_id: non_existing_record_id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 404 for non existing target topic id' do
+ post api_url, params: { source_topic_id: topic_3.id, target_topic_id: non_existing_record_id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 400 for identical topic ids' do
+ post api_url, params: { source_topic_id: topic_2.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eql('The source topic and the target topic are identical.')
+ end
+
+ it 'returns 400 if merge failed' do
+ allow_next_found_instance_of(Projects::Topic) do |topic|
+ allow(topic).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)
+ end
+
+ post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eql('Topics could not be merged!')
+ end
+ end
+
+ context 'as normal user' do
+ it 'returns 403 Forbidden' do
+ post api('/topics/merge', user), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'as anonymous' do
+ it 'returns 401 Unauthorized' do
+ post api('/topics/merge'), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 3ee895d9421..51c567309b7 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -218,8 +218,7 @@ RSpec.describe API::Unleash do
context 'with version 2 feature flags' do
it 'does not return a flag without any strategies' do
- create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
+ create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -228,10 +227,8 @@ RSpec.describe API::Unleash do
end
it 'returns a flag with a default strategy' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -248,10 +245,9 @@ RSpec.describe API::Unleash do
end
it 'returns a flag with a userWithId strategy' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user123,user456' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user123,user456' })
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -268,12 +264,13 @@ RSpec.describe API::Unleash do
end
it 'returns a flag with multiple strategies' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy_a = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user_a,user_b' })
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '45' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy_a = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user_a,user_b' })
+ strategy_b = create(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'default', percentage: '45' })
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
create(:operations_scope, strategy: strategy_b, environment_scope: 'production')
@@ -298,12 +295,12 @@ RSpec.describe API::Unleash do
end
it 'returns only flags matching the environment scope' do
- feature_flag_a = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
+ feature_flag_a = create(:operations_feature_flag,
+ project: project, name: 'feature1', active: true, version: 2)
strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
- feature_flag_b = create(:operations_feature_flag, project: project,
- name: 'feature2', active: true, version: 2)
+ feature_flag_b = create(:operations_feature_flag,
+ project: project, name: 'feature2', active: true, version: 2)
strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')
@@ -322,13 +319,11 @@ RSpec.describe API::Unleash do
end
it 'returns only strategies matching the environment scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy_a = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy_a = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -346,10 +341,12 @@ RSpec.describe API::Unleash do
it 'returns only flags for the given project' do
project_b = create(:project)
- feature_flag_a = create(:operations_feature_flag, project: project, name: 'feature_a', active: true, version: 2)
+ feature_flag_a = create(:operations_feature_flag,
+ project: project, name: 'feature_a', active: true, version: 2)
strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
create(:operations_scope, strategy: strategy_a, environment_scope: 'sandbox')
- feature_flag_b = create(:operations_feature_flag, project: project_b, name: 'feature_b', active: true, version: 2)
+ feature_flag_b = create(:operations_feature_flag,
+ project: project_b, name: 'feature_b', active: true, version: 2)
strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
create(:operations_scope, strategy: strategy_b, environment_scope: 'sandbox')
@@ -367,16 +364,16 @@ RSpec.describe API::Unleash do
end
it 'returns all strategies with a matching scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy_a = create(:operations_strategy, feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy_a = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
create(:operations_scope, strategy: strategy_a, environment_scope: '*')
- strategy_b = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy_b, environment_scope: 'review/*')
- strategy_c = create(:operations_strategy, feature_flag: feature_flag,
- name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '15' })
+ strategy_c = create(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'gradualRolloutUserId',
+ parameters: { groupId: 'default', percentage: '15' })
create(:operations_scope, strategy: strategy_c, environment_scope: 'review/patch-1')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'review/patch-1' }
@@ -395,10 +392,8 @@ RSpec.describe API::Unleash do
end
it 'returns a strategy with more than one matching scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'feature1', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
create(:operations_scope, strategy: strategy, environment_scope: '*')
@@ -416,10 +411,9 @@ RSpec.describe API::Unleash do
end
it 'returns a disabled flag with a matching scope' do
- feature_flag = create(:operations_feature_flag, project: project,
- name: 'myfeature', active: false, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'default', parameters: {})
+ feature_flag = create(:operations_feature_flag,
+ project: project, name: 'myfeature', active: false, version: 2)
+ strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
@@ -436,12 +430,12 @@ RSpec.describe API::Unleash do
end
it 'returns a userWithId strategy for a gitlabUserList strategy' do
- feature_flag = create(:operations_feature_flag, :new_version_flag, project: project,
- name: 'myfeature', active: true)
- user_list = create(:operations_feature_flag_user_list, project: project,
- name: 'My List', user_xids: 'user1,user2')
- strategy = create(:operations_strategy, feature_flag: feature_flag,
- name: 'gitlabUserList', parameters: {}, user_list: user_list)
+ feature_flag = create(:operations_feature_flag, :new_version_flag,
+ project: project, name: 'myfeature', active: true)
+ user_list = create(:operations_feature_flag_user_list,
+ project: project, name: 'My List', user_xids: 'user1,user2')
+ strategy = create(:operations_strategy,
+ feature_flag: feature_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list)
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }
diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb
index 69a8d865a59..6ce03954246 100644
--- a/spec/requests/api/usage_data_queries_spec.rb
+++ b/spec/requests/api/usage_data_queries_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
+require 'rake_helper'
RSpec.describe API::UsageDataQueries do
include UsageDataHelpers
@@ -64,5 +65,36 @@ RSpec.describe API::UsageDataQueries do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
+
+ context 'when querying sql metrics' do
+ let(:file) { Rails.root.join('tmp', 'test', 'sql_metrics_queries.json') }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/usage_data'
+
+ run_rake_task('gitlab:usage_data:generate_sql_metrics_queries')
+ end
+
+ after do
+ FileUtils.rm_rf(file)
+ end
+
+ it 'matches the generated query' do
+ Timecop.freeze(2021, 1, 1) do
+ get api(endpoint, admin)
+ end
+
+ data = Gitlab::Json.parse(File.read(file))
+
+ expect(
+ json_response['counts_monthly'].except('aggregated_metrics')
+ ).to eq(data['counts_monthly'].except('aggregated_metrics'))
+
+ expect(json_response['counts']).to eq(data['counts'])
+ expect(json_response['active_user_count']).to eq(data['active_user_count'])
+ expect(json_response['usage_activity_by_stage']).to eq(data['usage_activity_by_stage'])
+ expect(json_response['usage_activity_by_stage_monthly']).to eq(data['usage_activity_by_stage_monthly'])
+ end
+ end
end
end
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index ea50c404d92..d532fb6c168 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -138,7 +138,9 @@ RSpec.describe API::UsageData do
context 'with correct params' do
it 'returns status ok' do
- expect(Gitlab::Redis::HLL).to receive(:add)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track).with(anything, known_event, anything)
+ # allow other events to also get triggered
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track)
post api(endpoint, user), params: { event: known_event }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 26238a87209..96e23337411 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::Users do
+ include WorkhorseHelpers
+
let_it_be(:admin) { create(:admin) }
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
let_it_be(:key) { create(:key, user: user) }
@@ -116,7 +118,7 @@ RSpec.describe API::Users do
end
it "returns a 403 if the target user is an admin" do
- expect(TwoFactor::DestroyService).to receive(:new).never
+ expect(TwoFactor::DestroyService).not_to receive(:new)
expect do
patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin)
@@ -127,7 +129,7 @@ RSpec.describe API::Users do
end
it "returns a 404 if the target user cannot be found" do
- expect(TwoFactor::DestroyService).to receive(:new).never
+ expect(TwoFactor::DestroyService).not_to receive(:new)
patch api("/users/#{non_existing_record_id}/disable_two_factor", admin)
@@ -1180,6 +1182,22 @@ RSpec.describe API::Users do
expect(new_user.user_preference.view_diffs_file_by_file?).to eq(true)
end
+ it "creates user with avatar" do
+ workhorse_form_with_file(
+ api('/users', admin),
+ method: :post,
+ file_key: :avatar,
+ params: attributes_for(:user, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
+ )
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ new_user = User.find_by(id: json_response['id'])
+
+ expect(new_user).not_to eq(nil)
+ expect(json_response['avatar_url']).to include(new_user.avatar_path)
+ end
+
it "does not create user with invalid email" do
post api('/users', admin),
params: {
@@ -1478,7 +1496,12 @@ RSpec.describe API::Users do
end
it 'updates user with avatar' do
- put api("/users/#{user.id}", admin), params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ workhorse_form_with_file(
+ api("/users/#{user.id}", admin),
+ method: :put,
+ file_key: :avatar,
+ params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
+ )
user.reload
@@ -2479,14 +2502,32 @@ RSpec.describe API::Users do
describe "DELETE /users/:id" do
let_it_be(:issue) { create(:issue, author: user) }
- it "deletes user", :sidekiq_inline do
- namespace_id = user.namespace.id
+ context 'user deletion' do
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it "deletes user", :sidekiq_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)).to be_exists
+ end
+ end
- expect(response).to have_gitlab_http_status(:no_content)
- expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
- expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it "deletes user", :sidekiq_inline do
+ namespace_id = user.namespace.id
+
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
+ expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
end
context "sole owner of a group" do
@@ -2550,22 +2591,55 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:not_found)
end
- context "hard delete disabled" do
- it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ context 'hard delete' do
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ context "hard delete disabled" do
+ it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect(issue.reload).to be_persisted
- expect(issue.author.ghost?).to be_truthy
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(issue.reload).to be_persisted
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: false)).to be_exists
+ end
+ end
+
+ context "hard delete enabled" do
+ it "removes contributions", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)).to be_exists
+ end
+ end
end
- end
- context "hard delete enabled" do
- it "removes contributions", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
- expect(response).to have_gitlab_http_status(:no_content)
- expect(Issue.exists?(issue.id)).to be_falsy
+ context "hard delete disabled" do
+ it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(issue.reload).to be_persisted
+ expect(issue.author.ghost?).to be_truthy
+ end
+ end
+
+ context "hard delete enabled" do
+ it "removes contributions", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Issue.exists?(issue.id)).to be_falsy
+ end
+ end
end
end
end
@@ -3238,7 +3312,7 @@ RSpec.describe API::Users do
let(:user) { create(:user, **activity) }
context 'with no recent activity' do
- let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
+ let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago } }
it 'deactivates an active user' do
deactivate
@@ -3249,13 +3323,13 @@ RSpec.describe API::Users do
end
context 'with recent activity' do
- let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
+ let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago } }
it 'does not deactivate an active user' do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
- expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
+ expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
expect(user.reload.state).to eq('active')
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 77107d0b43c..81e923983ab 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -452,7 +452,7 @@ RSpec.describe 'Git HTTP requests' do
canonical_project.add_maintainer(user)
create(:merge_request,
source_project: project,
- target_project: canonical_project,
+ target_project: canonical_project,
source_branch: 'fixes',
allow_collaboration: true)
end
@@ -1105,7 +1105,7 @@ RSpec.describe 'Git HTTP requests' do
canonical_project.add_maintainer(user)
create(:merge_request,
source_project: project,
- target_project: canonical_project,
+ target_project: canonical_project,
source_branch: 'fixes',
allow_collaboration: true)
end
diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb
new file mode 100644
index 00000000000..9be013d4385
--- /dev/null
+++ b/spec/requests/groups/observability_controller_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::ObservabilityController do
+ include ContentSecurityPolicyHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ subject do
+ get group_observability_index_path(group)
+ response
+ end
+
+ describe 'GET #index' do
+ context 'when user is not authenticated' do
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when observability url is missing' do
+ before do
+ allow(described_class).to receive(:observability_url).and_return("")
+ end
+
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is not a developer' do
+ before do
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is authenticated and a developer' do
+ before do
+ sign_in(user)
+ group.add_developer(user)
+ end
+
+ it 'returns 200' do
+ expect(subject).to have_gitlab_http_status(:ok)
+ end
+
+ it 'renders the proper layout' do
+ expect(subject).to render_template("layouts/group")
+ expect(subject).to render_template("layouts/fullscreen")
+ expect(subject).not_to render_template('layouts/nav/breadcrumbs')
+ expect(subject).to render_template("nav/sidebar/_group")
+ end
+
+ describe 'iframe' do
+ subject do
+ get group_observability_index_path(group)
+ Nokogiri::HTML.parse(response.body).at_css('iframe#observability-ui-iframe')
+ end
+
+ it 'sets the iframe src to the proper URL' do
+ expect(subject.attributes['src'].value).to eq("https://observe.gitlab.com/-/#{group.id}")
+ end
+
+ it 'when the env is staging, sets the iframe src to the proper URL' do
+ stub_config_setting(url: Gitlab::Saas.staging_com_url)
+ expect(subject.attributes['src'].value).to eq("https://staging.observe.gitlab.com/-/#{group.id}")
+ end
+
+ it 'overrides the iframe src url if specified by OVERRIDE_OBSERVABILITY_URL env' do
+ stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test')
+
+ expect(subject.attributes['src'].value).to eq("http://foo.test/-/#{group.id}")
+ end
+ end
+
+ describe 'CSP' do
+ before do
+ setup_existing_csp_for_controller(described_class, csp)
+ end
+
+ subject do
+ get group_observability_index_path(group)
+ response.headers['Content-Security-Policy']
+ end
+
+ context 'when there is no CSP config' do
+ let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
+
+ it 'does not add any csp header' do
+ expect(subject).to be_blank
+ end
+ end
+
+ context 'when frame-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src 'https://something.test'
+ end
+ end
+
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test https://observe.gitlab.com 'self'")
+ end
+
+ it 'appends the proper url to frame-src CSP directives when Gilab.staging?' do
+ stub_config_setting(url: Gitlab::Saas.staging_com_url)
+
+ expect(subject).to include(
+ "frame-src https://something.test https://staging.observe.gitlab.com 'self'")
+ end
+
+ it 'appends the proper url to frame-src CSP directives when OVERRIDE_OBSERVABILITY_URL is specified' do
+ stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test')
+
+ expect(subject).to include(
+ "frame-src https://something.test http://foo.test 'self'")
+ end
+ end
+
+ context 'when self is already present in the policy' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src "'self'"
+ end
+ end
+
+ it 'does not append self again' do
+ expect(subject).to include(
+ "frame-src 'self' https://observe.gitlab.com;")
+ end
+ end
+
+ context 'when default-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something.test'
+ end
+ end
+
+ it 'does not change default-src' do
+ expect(subject).to include(
+ "default-src https://something.test;")
+ end
+
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test https://observe.gitlab.com 'self'")
+ end
+
+ it 'appends the proper url to frame-src CSP directives when Gilab.staging?' do
+ stub_config_setting(url: Gitlab::Saas.staging_com_url)
+
+ expect(subject).to include(
+ "frame-src https://something.test https://staging.observe.gitlab.com 'self'")
+ end
+
+ it 'appends the proper url to frame-src CSP directives when OVERRIDE_OBSERVABILITY_URL is specified' do
+ stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test')
+
+ expect(subject).to include(
+ "frame-src https://something.test http://foo.test 'self'")
+ end
+ end
+
+ context 'when frame-src and default-src exist in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something_default.test'
+ p.frame_src 'https://something.test'
+ end
+ end
+
+ it 'appends to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test https://observe.gitlab.com 'self'")
+ expect(subject).to include(
+ "default-src https://something_default.test")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb
index f70faf5bb9c..ae15b63df19 100644
--- a/spec/requests/health_controller_spec.rb
+++ b/spec/requests/health_controller_spec.rb
@@ -127,6 +127,10 @@ RSpec.describe HealthController do
end
it 'responds with readiness checks data' do
+ expect_next_instance_of(Gitlab::GitalyClient::ServerService) do |service|
+ expect(service).to receive(:readiness_check).and_return({ success: true })
+ end
+
subject
expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
@@ -138,19 +142,29 @@ RSpec.describe HealthController do
end
it 'responds with readiness checks data when a failure happens' do
- allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
- Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+ allow(Gitlab::HealthChecks::Redis::SharedStateCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('shared_state_check', false, "check error"))
subject
expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['redis_check']).to contain_exactly(
+ expect(json_response['shared_state_check']).to contain_exactly(
{ 'status' => 'failed', 'message' => 'check error' })
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
end
+ it 'checks all redis instances' do
+ expected_redis_checks = Gitlab::Redis::ALL_CLASSES.map do |redis|
+ { "#{redis.store_name.underscore}_check" => [{ 'status' => 'ok' }] }
+ end
+
+ subject
+
+ expect(json_response).to include(*expected_redis_checks)
+ end
+
context 'when DB is not accessible and connection raises an exception' do
before do
expect(Gitlab::HealthChecks::DbCheck)
@@ -170,7 +184,7 @@ RSpec.describe HealthController do
context 'when any exception happens during the probing' do
before do
- expect(Gitlab::HealthChecks::Redis::RedisCheck)
+ expect(Gitlab::HealthChecks::Redis::CacheCheck)
.to receive(:readiness)
.and_raise(::Redis::CannotConnectError, 'Redis down')
end
diff --git a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb
index 1e4628e5d59..12b9429b648 100644
--- a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb
+++ b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb
@@ -5,12 +5,6 @@ require 'spec_helper'
RSpec.describe JiraConnect::OauthCallbacksController do
describe 'GET /-/jira_connect/oauth_callbacks' do
context 'when logged in' do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
it 'renders a page prompting the user to close the window' do
get '/-/jira_connect/oauth_callbacks'
diff --git a/spec/requests/jira_connect/subscriptions_controller_spec.rb b/spec/requests/jira_connect/subscriptions_controller_spec.rb
index d8f329f13f5..f407ea09250 100644
--- a/spec/requests/jira_connect/subscriptions_controller_spec.rb
+++ b/spec/requests/jira_connect/subscriptions_controller_spec.rb
@@ -12,18 +12,29 @@ RSpec.describe JiraConnect::SubscriptionsController do
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
- before do
+ subject(:content_security_policy) do
get '/-/jira_connect/subscriptions', params: { jwt: jwt }
- end
- subject(:content_security_policy) { response.headers['Content-Security-Policy'] }
+ response.headers['Content-Security-Policy']
+ end
- it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_ids') }
+ it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/') }
+ it { is_expected.to include('http://self-managed-gitlab.com/api/') }
context 'with no self-managed instance configured' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }
- it { is_expected.not_to include('http://self-managed-gitlab.com') }
+ it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') }
+ it { is_expected.not_to include('http://self-managed-gitlab.com/api/') }
+ end
+
+ context 'with jira_connect_oauth_self_managed feature disabled' do
+ before do
+ stub_feature_flags(jira_connect_oauth_self_managed: false)
+ end
+
+ it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') }
+ it { is_expected.not_to include('http://self-managed-gitlab.com/api/') }
end
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index c9904ffa37b..e6916e02fde 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -27,6 +27,10 @@ RSpec.describe JwtController do
let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
it 'fails authentication' do
+ expect(::Gitlab::AuthLogger).to receive(:warn).with(
+ hash_including(message: 'JWT authentication failed',
+ http_user: 'personal_access_token')).and_call_original
+
get '/jwt/auth', params: parameters, headers: headers
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -80,7 +84,7 @@ RSpec.describe JwtController do
context 'project with enabled CI' do
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
+ it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build)).permit!) }
it_behaves_like 'user logging'
end
@@ -103,7 +107,12 @@ RSpec.describe JwtController do
it 'authenticates correctly' do
expect(response).to have_gitlab_http_status(:ok)
- expect(service_class).to have_received(:new).with(nil, nil, ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token)).permit!)
+ expect(service_class).to have_received(:new)
+ .with(
+ nil,
+ nil,
+ ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token)).permit!
+ )
end
it 'does not log a user' do
@@ -123,7 +132,12 @@ RSpec.describe JwtController do
it 'authenticates correctly' do
expect(response).to have_gitlab_http_status(:ok)
- expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!)
+ expect(service_class).to have_received(:new)
+ .with(
+ nil,
+ user,
+ ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token)).permit!
+ )
end
it_behaves_like 'rejecting a blocked user'
@@ -138,7 +152,7 @@ RSpec.describe JwtController do
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) }
+ it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap)).permit!) }
it_behaves_like 'rejecting a blocked user'
@@ -158,7 +172,7 @@ RSpec.describe JwtController do
ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
end
- it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
+ it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
it_behaves_like 'user logging'
end
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
index 180341fc85d..f2fb380bde0 100644
--- a/spec/requests/oauth_tokens_spec.rb
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -78,11 +78,12 @@ RSpec.describe 'OAuth Tokens requests' do
context 'revoked refresh token' do
let!(:existing_token) do
- create(:oauth_access_token, application: application,
- resource_owner_id: user.id,
- created_at: 2.hours.ago,
- revoked_at: 1.hour.ago,
- expires_in: 5)
+ create(:oauth_access_token,
+ application: application,
+ resource_owner_id: user.id,
+ created_at: 2.hours.ago,
+ revoked_at: 1.hour.ago,
+ expires_in: 5)
end
it 'does not issue a new token' do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index afaa6168bfd..3a40fec58e8 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -23,24 +23,24 @@ RSpec.describe 'OpenID Connect requests' do
let(:id_token_claims) do
{
- 'sub' => user.id.to_s,
+ 'sub' => user.id.to_s,
'sub_legacy' => hashed_subject
}
end
let(:user_info_claims) do
{
- 'name' => 'Alice',
- 'nickname' => 'alice',
- 'email' => 'public@example.com',
+ 'name' => 'Alice',
+ 'nickname' => 'alice',
+ 'email' => 'public@example.com',
'email_verified' => true,
- 'website' => 'https://example.com',
- 'profile' => 'http://localhost/alice',
- 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
- 'groups' => kind_of(Array),
- 'https://gitlab.org/claims/groups/owner' => kind_of(Array),
+ 'website' => 'https://example.com',
+ 'profile' => 'http://localhost/alice',
+ 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
+ 'groups' => kind_of(Array),
+ 'https://gitlab.org/claims/groups/owner' => kind_of(Array),
'https://gitlab.org/claims/groups/maintainer' => kind_of(Array),
- 'https://gitlab.org/claims/groups/developer' => kind_of(Array)
+ 'https://gitlab.org/claims/groups/developer' => kind_of(Array)
}
end
diff --git a/spec/requests/projects/environments_controller_spec.rb b/spec/requests/projects/environments_controller_spec.rb
index 0890b0c45da..66ab265fc0f 100644
--- a/spec/requests/projects/environments_controller_spec.rb
+++ b/spec/requests/projects/environments_controller_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Projects::EnvironmentsController do
deployer = create(:user)
pipeline = create(:ci_pipeline, project: environment.project)
build = create(:ci_build, environment: environment.name, pipeline: pipeline, user: deployer)
- create(:deployment, :success, environment: environment, deployable: build, user: deployer,
- project: project, sha: commit.sha)
+ create(:deployment, :success,
+ environment: environment, deployable: build, user: deployer, project: project, sha: commit.sha)
end
end
diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
index 08d4ad2f9ba..41593b8d7a7 100644
--- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
@@ -2,9 +2,6 @@
require 'spec_helper'
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
-
RSpec.describe Projects::GoogleCloud::ConfigurationController do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_configuration_path(project) }
@@ -29,10 +26,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
get url
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::ConfigurationController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: unauthorized_member
)
@@ -56,7 +52,7 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
context 'but gitlab instance is not configured for google oauth2' do
it 'returns forbidden' do
- unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
+ unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret).new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
@@ -68,11 +64,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
expect(response).to have_gitlab_http_status(:forbidden)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'google_oauth2_enabled!',
- label: 'error_access_denied',
- extra: { reason: 'google_oauth2_not_configured',
- config: unconfigured_google_oauth2 },
+ category: 'Projects::GoogleCloud::ConfigurationController',
+ action: 'error_google_oauth2_not_enabled',
+ label: nil,
project: project,
user: authorized_member
)
@@ -93,10 +87,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'feature_flag_enabled!',
- label: 'error_access_denied',
- property: 'feature_flag_not_enabled',
+ category: 'Projects::GoogleCloud::ConfigurationController',
+ action: 'error_feature_flag_not_enabled',
+ label: nil,
project: project,
user: authorized_member
)
@@ -117,20 +110,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
expect(response).to be_successful
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'configuration#index',
- label: 'success',
- extra: {
- configurationUrl: project_google_cloud_configuration_path(project),
- deploymentsUrl: project_google_cloud_deployments_path(project),
- databasesUrl: project_google_cloud_databases_path(project),
- serviceAccounts: [],
- createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
- emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
- configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
- gcpRegions: [],
- revokeOauthUrl: nil
- },
+ category: 'Projects::GoogleCloud::ConfigurationController',
+ action: 'render_page',
+ label: nil,
project: project,
user: authorized_member
)
diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb
index c9335f8f317..4edef71f326 100644
--- a/spec/requests/projects/google_cloud/databases_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb
@@ -2,133 +2,166 @@
require 'spec_helper'
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
+RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow do
+ shared_examples 'shared examples for database controller endpoints' do
+ include_examples 'requires `admin_project_google_cloud` role'
-RSpec.describe Projects::GoogleCloud::DatabasesController do
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:url) { project_google_cloud_databases_path(project) }
+ include_examples 'requires feature flag `incubation_5mp_google_cloud` enabled'
- let_it_be(:user_guest) { create(:user) }
- let_it_be(:user_developer) { create(:user) }
- let_it_be(:user_maintainer) { create(:user) }
+ include_examples 'requires valid Google OAuth2 configuration'
- let_it_be(:unauthorized_members) { [user_guest, user_developer] }
- let_it_be(:authorized_members) { [user_maintainer] }
+ include_examples 'requires valid Google Oauth2 token' do
+ let_it_be(:mock_gcp_projects) { [{}, {}, {}] }
+ let_it_be(:mock_branches) { [] }
+ let_it_be(:mock_tags) { [] }
+ end
+ end
+
+ context '-/google_cloud/databases' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/index' }
+ let_it_be(:redirects_to) { nil }
- before do
- project.add_guest(user_guest)
- project.add_developer(user_developer)
- project.add_maintainer(user_maintainer)
+ subject { get project_google_cloud_databases_path(project) }
+
+ include_examples 'shared examples for database controller endpoints'
end
- context 'when accessed by unauthorized members' do
- it 'returns not found on GET request' do
- unauthorized_members.each do |unauthorized_member|
- sign_in(unauthorized_member)
+ context '-/google_cloud/databases/new/postgres' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
+ let_it_be(:redirects_to) { nil }
- get url
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
- project: project,
- user: unauthorized_member
- )
+ subject { get new_project_google_cloud_database_path(project, :postgres) }
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ include_examples 'shared examples for database controller endpoints'
end
- context 'when accessed by authorized members' do
- it 'returns successful' do
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ context '-/google_cloud/databases/new/mysql' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
+ let_it_be(:redirects_to) { nil }
- get url
+ subject { get new_project_google_cloud_database_path(project, :mysql) }
- expect(response).to be_successful
- expect(response).to render_template('projects/google_cloud/databases/index')
- end
- end
+ include_examples 'shared examples for database controller endpoints'
+ end
- context 'but gitlab instance is not configured for google oauth2' do
- it 'returns forbidden' do
- unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
- allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
- .with('google_oauth2')
- .and_return(unconfigured_google_oauth2)
+ context '-/google_cloud/databases/new/sqlserver' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
+ let_it_be(:redirects_to) { nil }
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ subject { get new_project_google_cloud_database_path(project, :sqlserver) }
- get url
+ include_examples 'shared examples for database controller endpoints'
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'google_oauth2_enabled!',
- label: 'error_access_denied',
- extra: { reason: 'google_oauth2_not_configured',
- config: unconfigured_google_oauth2 },
- project: project,
- user: authorized_member
- )
+ context '-/google_cloud/databases/create' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { nil }
+ let_it_be(:redirects_to) { project_google_cloud_databases_path(project) }
+
+ subject { post project_google_cloud_databases_path(project) }
+
+ include_examples 'shared examples for database controller endpoints'
+
+ context 'when the request is valid' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ allow(client).to receive(:list_projects).and_return(mock_gcp_projects)
+ end
+
+ allow_next_instance_of(BranchesFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches)
+ end
+
+ allow_next_instance_of(TagsFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches)
end
end
- end
- context 'but feature flag is disabled' do
- before do
- stub_feature_flags(incubation_5mp_google_cloud: false)
+ subject do
+ post project_google_cloud_databases_path(project)
end
- it 'returns not found' do
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ it 'calls EnableCloudsqlService and redirects on error' do
+ expect_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
+ expect(service).to receive(:execute)
+ .and_return({ status: :error, message: 'error' })
+ end
- get url
+ subject
- expect(response).to have_gitlab_http_status(:not_found)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'feature_flag_enabled!',
- label: 'error_access_denied',
- property: 'feature_flag_not_enabled',
- project: project,
- user: authorized_member
- )
- end
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
+
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DatabasesController',
+ action: 'error_enable_cloudsql_services',
+ label: nil,
+ project: project,
+ user: user
+ )
end
- end
- context 'but google oauth2 token is not valid' do
- it 'does not return revoke oauth url' do
- allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
- allow(client).to receive(:validate_token).and_return(false)
+ context 'when EnableCloudsqlService is successful' do
+ before do
+ allow_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
+ allow(service).to receive(:execute)
+ .and_return({ status: :success, message: 'success' })
+ end
end
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ it 'calls CreateCloudsqlInstanceService and redirects on error' do
+ expect_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
+ expect(service).to receive(:execute)
+ .and_return({ status: :error, message: 'error' })
+ end
+
+ subject
- get url
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
- expect(response).to be_successful
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'databases#index',
- label: 'success',
- extra: {
- configurationUrl: project_google_cloud_configuration_path(project),
- deploymentsUrl: project_google_cloud_deployments_path(project),
- databasesUrl: project_google_cloud_databases_path(project)
- },
+ category: 'Projects::GoogleCloud::DatabasesController',
+ action: 'error_create_cloudsql_instance',
+ label: nil,
project: project,
- user: authorized_member
+ user: user
)
end
+
+ context 'when CreateCloudsqlInstanceService is successful' do
+ before do
+ allow_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
+ allow(service).to receive(:execute)
+ .and_return({ status: :success, message: 'success' })
+ end
+ end
+
+ it 'redirects as expected' do
+ subject
+
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
+
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DatabasesController',
+ action: 'create_cloudsql_instance',
+ label: "{}",
+ project: project,
+ user: user
+ )
+ end
+ end
end
end
end
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index 9e854e01516..ad6a3912e0b 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -29,10 +29,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: nil
)
@@ -48,10 +47,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: nil
)
@@ -75,6 +73,30 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
end
+ describe 'Authorized GET project/-/google_cloud/deployments', :snowplow do
+ before do
+ sign_in(user_maintainer)
+
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ end
+ end
+
+ it 'renders template' do
+ get "#{project_google_cloud_deployments_path(project)}"
+
+ expect(response).to render_template(:index)
+
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'render_page',
+ label: nil,
+ project: project,
+ user: user_maintainer
+ )
+ end
+ end
+
describe 'Authorized GET project/-/google_cloud/deployments/cloud_run', :snowplow do
let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
@@ -92,11 +114,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to redirect_to(project_google_cloud_deployments_path(project))
# since GPC_PROJECT_ID is not set, enable cloud run service should return an error
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'deployments#cloud_run',
- label: 'error_enable_cloud_run',
- extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.',
- status: :error },
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_enable_services',
+ label: nil,
project: project,
user: user_maintainer
)
@@ -113,10 +133,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to redirect_to(project_google_cloud_deployments_path(project))
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'deployments#cloud_run',
- label: 'error_gcp',
- extra: mock_gcp_error,
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_google_api',
+ label: nil,
project: project,
user: user_maintainer
)
@@ -136,10 +155,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to redirect_to(project_google_cloud_deployments_path(project))
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'deployments#cloud_run',
- label: 'error_generate_pipeline',
- extra: { status: :error },
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_generate_cloudrun_pipeline',
+ label: nil,
project: project,
user: user_maintainer
)
@@ -159,15 +177,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to include(project_new_merge_request_path(project))
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'deployments#cloud_run',
- label: 'success',
- extra: { "title": "Enable deployments to Cloud Run",
- "description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n",
- "source_project_id": project.id,
- "target_project_id": project.id,
- "source_branch": nil,
- "target_branch": project.default_branch },
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'generate_cloudrun_pipeline',
+ label: nil,
project: project,
user: user_maintainer
)
diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
index f88273080d5..e77bcdb40b8 100644
--- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
@@ -13,10 +13,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
it "tracks event" do
is_expected.to be(404)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::GcpRegionsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: nil
)
@@ -27,10 +26,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
it "tracks event" do
is_expected.to be(404)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::GcpRegionsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: nil
)
@@ -41,10 +39,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
it "tracks event" do
is_expected.to be(404)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'feature_flag_enabled!',
- label: 'error_access_denied',
- property: 'feature_flag_not_enabled',
+ category: 'Projects::GoogleCloud::GcpRegionsController',
+ action: 'error_feature_flag_not_enabled',
+ label: nil,
project: project,
user: user_maintainer
)
@@ -55,10 +52,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
it "tracks event" do
is_expected.to be(403)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'google_oauth2_enabled!',
- label: 'error_access_denied',
- extra: { reason: 'google_oauth2_not_configured', config: config },
+ category: 'Projects::GoogleCloud::GcpRegionsController',
+ action: 'error_google_oauth2_not_enabled',
+ label: nil,
project: project,
user: user_maintainer
)
diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
index 36441a184cb..9bd8468767d 100644
--- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
@@ -50,10 +50,9 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do
expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:notice]).to eq('Google OAuth2 token revocation requested')
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'revoke_oauth#create',
- label: 'success',
- property: '{}',
+ category: 'Projects::GoogleCloud::RevokeOauthController',
+ action: 'revoke_oauth',
+ label: nil,
project: project,
user: user
)
@@ -73,10 +72,9 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do
expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed')
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'revoke_oauth#create',
- label: 'error',
- property: '{}',
+ category: 'Projects::GoogleCloud::RevokeOauthController',
+ action: 'error',
+ label: nil,
project: project,
user: user
)
diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
index ae2519855db..133c6f9153d 100644
--- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
@@ -30,10 +30,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::ServiceAccountsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: nil
)
@@ -53,10 +52,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
get url
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::ServiceAccountsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: unauthorized_member
)
@@ -71,10 +69,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
post url
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
+ category: 'Projects::GoogleCloud::ServiceAccountsController',
+ action: 'error_invalid_user',
+ label: nil,
project: project,
user: unauthorized_member
)
@@ -135,10 +132,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:warning]).to eq('No Google Cloud projects - You need at least one Google Cloud project')
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'service_accounts#index',
- label: 'error_form',
- property: 'no_gcp_projects',
+ category: 'Projects::GoogleCloud::ServiceAccountsController',
+ action: 'error_no_gcp_projects',
+ label: nil,
project: project,
user: authorized_member
)
@@ -207,11 +203,10 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:warning]).to eq('Google Cloud Error - client-error')
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'service_accounts#index',
- label: 'error_gcp',
- extra: google_client_error,
+ category: 'Projects::GoogleCloud::ServiceAccountsController',
+ action: 'error_google_api',
project: project,
+ label: nil,
user: authorized_member
)
end
@@ -226,10 +221,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:warning]).to eq('Google Cloud Error - client-error')
expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'service_accounts#create',
- label: 'error_gcp',
- extra: google_client_error,
+ category: 'Projects::GoogleCloud::ServiceAccountsController',
+ action: 'error_google_api',
+ label: nil,
project: project,
user: authorized_member
)
diff --git a/spec/requests/projects/hook_logs_controller_spec.rb b/spec/requests/projects/hook_logs_controller_spec.rb
new file mode 100644
index 00000000000..8b3ec307e53
--- /dev/null
+++ b/spec/requests/projects/hook_logs_controller_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::HookLogsController do
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_refind(:web_hook) { create(:project_hook) }
+ let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
+
+ let(:project) { web_hook.project }
+
+ it_behaves_like WebHooks::HookLogActions do
+ let(:edit_hook_path) { edit_project_hook_url(project, web_hook) }
+
+ before do
+ project.add_owner(user)
+ end
+ end
+end
diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
index 7be863aae75..c859e91e21a 100644
--- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
}
end
- def go(extra_params = {})
+ def go(headers: {}, **extra_params)
params = {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -56,10 +56,20 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
get diffs_batch_namespace_project_json_merge_request_path(params.merge(extra_params)), headers: headers
end
- context 'with caching', :use_clean_rails_memory_store_caching do
- subject { go(page: 0, per_page: 5) }
+ context 'without caching' do
+ subject { go(headers: headers, page: 0, per_page: 5) }
+
+ let(:headers) { {} }
+ let(:collection) { Gitlab::Diff::FileCollection::Compare }
+ let(:expected_options) { collection_arguments }
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
context 'when the request has not been cached' do
+ subject { go(headers: { 'If-None-Match' => '' }, page: 0, per_page: 5) }
+
it_behaves_like 'serializes diffs with expected arguments' do
let(:collection) { Gitlab::Diff::FileCollection::Compare }
let(:expected_options) { collection_arguments }
@@ -67,16 +77,18 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
end
context 'when the request has already been cached' do
+ subject { go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) }
+
before do
go(page: 0, per_page: 5)
end
it 'does not serialize diffs' do
- expect_next_instance_of(PaginatedDiffSerializer) do |instance|
- expect(instance).not_to receive(:represent)
- end
+ expect(PaginatedDiffSerializer).not_to receive(:new)
+
+ go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5)
- subject
+ expect(response).to have_gitlab_http_status(:not_modified)
end
context 'with the different user' do
diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb
index 937b0f1d713..9f0b9a9cb1b 100644
--- a/spec/requests/projects/merge_requests/diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_spec.rb
@@ -53,247 +53,154 @@ RSpec.describe 'Merge Requests Diffs' do
get diffs_batch_namespace_project_json_merge_request_path(params.merge(extra_params)), headers: headers
end
- context 'with caching', :use_clean_rails_memory_store_caching do
+ context 'without caching' do
subject { go(headers: headers, page: 0, per_page: 5) }
let(:headers) { {} }
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20) }
- context 'when the request has not been cached' do
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20) }
-
- it_behaves_like 'serializes diffs with expected arguments'
- end
-
- context 'when the request has already been cached' do
- before do
- go(page: 0, per_page: 5)
- end
-
- it 'does not serialize diffs' do
- expect_next_instance_of(PaginatedDiffSerializer) do |instance|
- expect(instance).not_to receive(:represent)
- end
-
- subject
- end
-
- context 'when using ETags' do
- context 'when etag_merge_request_diff_batches is true' do
- let(:headers) { { 'If-None-Match' => response.etag } }
-
- it 'does not serialize diffs' do
- expect(PaginatedDiffSerializer).not_to receive(:new)
-
- go(headers: headers, page: 0, per_page: 5)
-
- expect(response).to have_gitlab_http_status(:not_modified)
- end
- end
-
- context 'when etag_merge_request_diff_batches is false' do
- let(:headers) { { 'If-None-Match' => response.etag } }
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- before do
- stub_feature_flags(etag_merge_request_diff_batches: false)
- end
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ subject { go(headers: headers, page: 0, per_page: 5) }
- it 'does not serialize diffs' do
- expect_next_instance_of(PaginatedDiffSerializer) do |instance|
- expect(instance).not_to receive(:represent)
- end
+ let(:headers) { { 'If-None-Match' => response.etag } }
- subject
+ before do
+ go(page: 0, per_page: 5)
+ end
- expect(response).to have_gitlab_http_status(:success)
- end
- end
- end
+ it 'does not serialize diffs' do
+ expect(PaginatedDiffSerializer).not_to receive(:new)
- context 'with the different user' do
- let(:another_user) { create(:user) }
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20) }
+ go(headers: headers, page: 0, per_page: 5)
- before do
- project.add_maintainer(another_user)
- sign_in(another_user)
- end
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
- it_behaves_like 'serializes diffs with expected arguments'
+ context 'with the different user' do
+ let(:another_user) { create(:user) }
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20) }
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
+ before do
+ project.add_maintainer(another_user)
+ sign_in(another_user)
end
- context 'with a new unfoldable diff position' do
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20) }
-
- let(:unfoldable_position) do
- create(:diff_position)
- end
-
- before do
- expect_next_instance_of(Gitlab::Diff::PositionCollection) do |instance|
- expect(instance)
- .to receive(:unfoldable)
- .and_return([unfoldable_position])
- end
- end
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- it_behaves_like 'serializes diffs with expected arguments'
+ context 'with a new unfoldable diff position' do
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20) }
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
+ let(:unfoldable_position) do
+ create(:diff_position)
end
- context 'with disabled display_merge_conflicts_in_diff feature' do
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) }
-
- before do
- stub_feature_flags(display_merge_conflicts_in_diff: false)
- end
-
- it_behaves_like 'serializes diffs with expected arguments'
-
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
+ before do
+ expect_next_instance_of(Gitlab::Diff::PositionCollection) do |instance|
+ expect(instance)
+ .to receive(:unfoldable)
+ .and_return([unfoldable_position])
end
end
- context 'with diff_head option' do
- subject { go(page: 0, per_page: 5, diff_head: true) }
-
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_ref_head_diff: true) }
-
- before do
- merge_request.create_merge_head_diff!
- end
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- it_behaves_like 'serializes diffs with expected arguments'
+ context 'with disabled display_merge_conflicts_in_diff feature' do
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) }
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
+ before do
+ stub_feature_flags(display_merge_conflicts_in_diff: false)
end
- context 'with the different pagination option' do
- subject { go(page: 5, per_page: 5) }
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20) }
+ context 'with diff_head option' do
+ subject { go(page: 0, per_page: 5, diff_head: true) }
- it_behaves_like 'serializes diffs with expected arguments'
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_ref_head_diff: true) }
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
+ before do
+ merge_request.create_merge_head_diff!
end
- context 'with the different diff_view' do
- subject { go(page: 0, per_page: 5, view: :parallel) }
-
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(diff_view: :parallel) }
-
- it_behaves_like 'serializes diffs with expected arguments'
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
- end
+ context 'with the different pagination option' do
+ subject { go(page: 5, per_page: 5) }
- context 'with the different expanded option' do
- subject { go(page: 0, per_page: 5, expanded: true ) }
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20) }
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20) }
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- it_behaves_like 'serializes diffs with expected arguments'
+ context 'with the different diff_view' do
+ subject { go(page: 0, per_page: 5, view: :parallel) }
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
- end
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20).merge(diff_view: :parallel) }
- context 'with the different ignore_whitespace_change option' do
- subject { go(page: 0, per_page: 5, w: 1) }
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
- let(:collection) { Gitlab::Diff::FileCollection::Compare }
- let(:expected_options) { collection_arguments(total_pages: 20) }
+ context 'with the different expanded option' do
+ subject { go(page: 0, per_page: 5, expanded: true ) }
- it_behaves_like 'serializes diffs with expected arguments'
+ let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
+ let(:expected_options) { collection_arguments(total_pages: 20) }
- context 'when using ETag caching' do
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:headers) { { 'If-None-Match' => response.etag } }
- end
- end
- end
+ it_behaves_like 'serializes diffs with expected arguments'
end
- context 'when the paths is given' do
- subject { go(headers: headers, page: 0, per_page: 5, paths: %w[README CHANGELOG]) }
-
- before do
- go(page: 0, per_page: 5, paths: %w[README CHANGELOG])
- end
+ context 'with the different ignore_whitespace_change option' do
+ subject { go(page: 0, per_page: 5, w: 1) }
- context 'when using ETag caching' do
- let(:headers) { { 'If-None-Match' => response.etag } }
+ let(:collection) { Gitlab::Diff::FileCollection::Compare }
+ let(:expected_options) { collection_arguments(total_pages: 20) }
- context 'when etag_merge_request_diff_batches is true' do
- it 'does not serialize diffs' do
- expect(PaginatedDiffSerializer).not_to receive(:new)
+ it_behaves_like 'serializes diffs with expected arguments'
+ end
+ end
- subject
+ context 'when the paths is given' do
+ subject { go(headers: headers, page: 0, per_page: 5, paths: %w[README CHANGELOG]) }
- expect(response).to have_gitlab_http_status(:not_modified)
- end
- end
+ before do
+ go(page: 0, per_page: 5, paths: %w[README CHANGELOG])
+ end
- context 'when etag_merge_request_diff_batches is false' do
- before do
- stub_feature_flags(etag_merge_request_diff_batches: false)
- end
+ context 'when using ETag caching' do
+ let(:headers) { { 'If-None-Match' => response.etag } }
- it 'does not use cache' do
- expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
+ it 'does not serialize diffs' do
+ expect(PaginatedDiffSerializer).not_to receive(:new)
- subject
+ subject
- expect(response).to have_gitlab_http_status(:success)
- end
- end
+ expect(response).to have_gitlab_http_status(:not_modified)
end
+ end
- context 'when not using ETag caching' do
- it 'does not use cache' do
- expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
+ context 'when not using ETag caching' do
+ let(:headers) { {} }
- subject
+ it 'does not use cache' do
+ expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
- expect(response).to have_gitlab_http_status(:success)
- end
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
end
end
end
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index 9503dafcf2a..305ca6147be 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -37,12 +37,10 @@ RSpec.describe 'merge requests discussions' do
it 'avoids N+1 DB queries', :request_store do
send_request # warm up
- create(:diff_note_on_merge_request, noteable: merge_request,
- project: merge_request.project)
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
control = ActiveRecord::QueryRecorder.new { send_request }
- create(:diff_note_on_merge_request, noteable: merge_request,
- project: merge_request.project)
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
expect do
send_request
@@ -51,8 +49,7 @@ RSpec.describe 'merge requests discussions' do
it 'limits Gitaly queries', :request_store do
Gitlab::GitalyClient.allow_n_plus_1_calls do
- create_list(:diff_note_on_merge_request, 7, noteable: merge_request,
- project: merge_request.project)
+ create_list(:diff_note_on_merge_request, 7, noteable: merge_request, project: merge_request.project)
end
# The creations above write into the Gitaly counts
diff --git a/spec/requests/projects/packages/package_files_controller_spec.rb b/spec/requests/projects/packages/package_files_controller_spec.rb
new file mode 100644
index 00000000000..a6daf57f0fa
--- /dev/null
+++ b/spec/requests/projects/packages/package_files_controller_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Packages::PackageFilesController do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:package) { create(:package, project: project) }
+ let_it_be(:package_file) { create(:package_file, package: package) }
+
+ let(:filename) { package_file.file_name }
+
+ describe 'GET download' do
+ subject do
+ get download_namespace_project_package_file_url(
+ id: package_file.id,
+ namespace_id: project.namespace,
+ project_id: project
+ )
+ end
+
+ it 'sends the package file' do
+ subject
+
+ expect(response.headers['Content-Disposition'])
+ .to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ end
+
+ it_behaves_like 'bumping the package last downloaded at field'
+ end
+end
diff --git a/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb
new file mode 100644
index 00000000000..77daff901a1
--- /dev/null
+++ b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Settings::IntegrationHookLogsController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:integration) { create(:datadog_integration) }
+ let_it_be_with_refind(:web_hook) { integration.service_hook }
+ let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
+
+ let(:project) { integration.project }
+
+ it_behaves_like WebHooks::HookLogActions do
+ let(:edit_hook_path) { edit_project_settings_integration_url(project, integration) }
+
+ before do
+ project.add_owner(user)
+ end
+ end
+end
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index 2f249952455..e8d3e94bd0e 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
it 'sends an email' do
mail = find_email_for(user)
expect(mail.to).to match_array([user.email])
- expect(mail.subject).to eq('Verify your identity')
+ expect(mail.subject).to eq(s_('IdentityVerification|Verify your identity'))
end
end
@@ -50,7 +50,7 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
it 'adds a verification error message' do
expect(response.body)
.to include("You&#39;ve reached the maximum amount of tries. "\
- 'Wait 10 minutes or resend a new code and try again.')
+ 'Wait 10 minutes or send a new code and try again.')
end
end
@@ -62,7 +62,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
it_behaves_like 'prompt for email verification'
it 'adds a verification error message' do
- expect(response.body).to include(('The code is incorrect. Enter it again, or resend a new code.'))
+ expect(response.body)
+ .to include((s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')))
end
end
@@ -75,7 +76,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
it_behaves_like 'prompt for email verification'
it 'adds a verification error message' do
- expect(response.body).to include(('The code has expired. Resend a new code and try again.'))
+ expect(response.body)
+ .to include((s_('IdentityVerification|The code has expired. Send a new code and try again.')))
end
end
@@ -112,7 +114,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
it 'redirects to the login form and shows an alert message' do
expect(response).to redirect_to(new_user_session_path)
- expect(flash[:alert]).to eq('Maximum login attempts exceeded. Wait 10 minutes and try again.')
+ expect(flash[:alert])
+ .to eq(s_('IdentityVerification|Maximum login attempts exceeded. Wait 10 minutes and try again.'))
end
end
@@ -217,6 +220,7 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
describe 'successful_verification' do
before do
+ allow(user).to receive(:role_required?).and_return(true) # It skips the required signup info before_action
sign_in(user)
end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 9f5f821cc61..ae69b222280 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -71,6 +71,10 @@ RSpec.shared_examples 'groups routing' do
it 'routes to the harbor tags controller' do
expect(get("groups/#{group_path}/-/harbor/repositories/test/artifacts/test/tags")).to route_to('groups/harbor/tags#index', group_id: group_path, repository_id: 'test', artifact_id: 'test')
end
+
+ it 'routes to the observability controller' do
+ expect(get("groups/#{group_path}/-/observability")).to route_to('groups/observability#index', group_id: group_path)
+ end
end
RSpec.describe "Groups", "routing" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index f701dd9c488..9317a661188 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -480,7 +480,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/blame/master/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/blame', action: 'show',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/#{newline_file}" })
@@ -499,7 +499,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/blob/blob/master/blob/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/blob', action: 'show',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "blob/master/blob/#{newline_file}" })
@@ -520,7 +520,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/tree/master/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/tree', action: 'show',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/#{newline_file}" })
@@ -540,7 +540,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/find_file/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/find_file', action: 'show',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "#{newline_file}" })
@@ -551,7 +551,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/files/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/find_file', action: 'list',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "#{newline_file}" })
@@ -570,7 +570,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/edit/master/docs/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/blob', action: 'edit',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/docs/#{newline_file}" })
@@ -584,7 +584,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/edit/master/docs/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/blob', action: 'edit',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/docs/#{newline_file}" })
@@ -600,7 +600,7 @@ RSpec.describe 'project routing' do
newline_file = "new\n\nline.txt"
url_encoded_newline_file = ERB::Util.url_encode(newline_file)
assert_routing({ path: "/gitlab/gitlabhq/-/raw/master/#{url_encoded_newline_file}",
- method: :get },
+ method: :get },
{ controller: 'projects/raw', action: 'show',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/#{newline_file}" })
@@ -889,8 +889,8 @@ RSpec.describe 'project routing' do
describe Projects::Snippets::BlobsController, "routing" do
it "to #raw" do
expect(get('/gitlab/gitlabhq/-/snippets/1/raw/master/lib/version.rb'))
- .to route_to('projects/snippets/blobs#raw', namespace_id: 'gitlab',
- project_id: 'gitlabhq', snippet_id: '1', ref: 'master', path: 'lib/version.rb')
+ .to route_to('projects/snippets/blobs#raw',
+ namespace_id: 'gitlab', project_id: 'gitlabhq', snippet_id: '1', ref: 'master', path: 'lib/version.rb')
end
end
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
index 41646d1b515..9eb421ec7d0 100644
--- a/spec/routing/uploads_routing_spec.rb
+++ b/spec/routing/uploads_routing_spec.rb
@@ -39,12 +39,4 @@ RSpec.describe 'Uploads', 'routing' do
expect(post("/uploads/#{model}?id=1")).not_to be_routable
end
end
-
- describe 'legacy paths' do
- include RSpec::Rails::RequestExampleGroup
-
- it 'redirects project uploads to canonical path under project namespace' do
- expect(get('/uploads/namespace/project/12345/test.png')).to redirect_to('/namespace/project/uploads/12345/test.png')
- end
- end
end
diff --git a/spec/rubocop/check_graceful_task_spec.rb b/spec/rubocop/check_graceful_task_spec.rb
new file mode 100644
index 00000000000..0364820a602
--- /dev/null
+++ b/spec/rubocop/check_graceful_task_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'stringio'
+
+require_relative '../support/helpers/next_instance_of'
+require_relative '../../rubocop/check_graceful_task'
+
+RSpec.describe RuboCop::CheckGracefulTask do
+ include NextInstanceOf
+
+ let(:output) { StringIO.new }
+
+ subject(:task) { described_class.new(output) }
+
+ describe '#run' do
+ let(:status_success) { RuboCop::CLI::STATUS_SUCCESS }
+ let(:status_offenses) { RuboCop::CLI::STATUS_OFFENSES }
+ let(:rubocop_status) { status_success }
+ let(:adjusted_rubocop_status) { rubocop_status }
+
+ subject { task.run(args) }
+
+ before do
+ # Don't notify Slack accidentally.
+ allow(Gitlab::Popen).to receive(:popen).and_raise('Notifications forbidden.')
+ stub_const('ENV', ENV.to_hash.delete_if { |key, _| key.start_with?('CI_') })
+
+ allow_next_instance_of(RuboCop::CLI) do |cli|
+ allow(cli).to receive(:run).and_return(rubocop_status)
+ end
+
+ allow(RuboCop::Formatter::GracefulFormatter)
+ .to receive(:adjusted_exit_status).and_return(adjusted_rubocop_status)
+ end
+
+ shared_examples 'rubocop scan' do |rubocop_args:|
+ it 'invokes a RuboCop scan' do
+ rubocop_options = %w[--parallel --format RuboCop::Formatter::GracefulFormatter]
+ rubocop_options.concat(rubocop_args)
+
+ expect_next_instance_of(RuboCop::CLI) do |cli|
+ expect(cli).to receive(:run).with(rubocop_options).and_return(rubocop_status)
+ end
+
+ subject
+
+ expect(output.string)
+ .to include('Running RuboCop in graceful mode:')
+ .and include("rubocop #{rubocop_options.join(' ')}")
+ .and include('This might take a while...')
+ end
+ end
+
+ context 'without args' do
+ let(:args) { [] }
+
+ it_behaves_like 'rubocop scan', rubocop_args: []
+
+ context 'with adjusted rubocop status' do
+ let(:rubocop_status) { status_offenses }
+ let(:adjusted_rubocop_status) { status_success }
+
+ context 'with sufficient environment variables' do
+ let(:channel) { 'f_rubocop' }
+
+ before do
+ env = {
+ 'CI_SLACK_WEBHOOK_URL' => 'webhook_url',
+ 'CI_JOB_NAME' => 'job_name',
+ 'CI_JOB_URL' => 'job_url'
+ }
+
+ stub_const('ENV', ENV.to_hash.update(env))
+ end
+
+ it 'notifies slack' do
+ popen_args = ['scripts/slack', channel, kind_of(String), 'rubocop', kind_of(String)]
+ popen_result = ['', 0]
+ expect(Gitlab::Popen).to receive(:popen).with(popen_args).and_return(popen_result)
+
+ subject
+
+ expect(output.string).to include("Notifying Slack ##{channel}.")
+ end
+
+ context 'with when notification fails' do
+ it 'prints that notification failed' do
+ popen_result = ['', 1]
+ expect(Gitlab::Popen).to receive(:popen).and_return(popen_result)
+
+ subject
+
+ expect(output.string).to include("Failed to notify Slack channel ##{channel}.")
+ end
+ end
+ end
+
+ context 'with missing environment variables' do
+ it 'skips slack notification' do
+ expect(Gitlab::Popen).not_to receive(:popen)
+
+ subject
+
+ expect(output.string).to include('Skipping Slack notification.')
+ end
+ end
+ end
+ end
+
+ context 'with args' do
+ let(:args) { %w[a.rb Lint/EmptyFile b.rb Lint/Syntax] }
+
+ it_behaves_like 'rubocop scan', rubocop_args: %w[--only Lint/EmptyFile,Lint/Syntax a.rb b.rb]
+
+ it 'does not notify slack' do
+ expect(Gitlab::Popen).not_to receive(:popen)
+
+ subject
+
+ expect(output.string).not_to include('Skipping Slack notification.')
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/code_reuse_helpers_spec.rb b/spec/rubocop/code_reuse_helpers_spec.rb
index 0d06d37d67a..a112c9754f3 100644
--- a/spec/rubocop/code_reuse_helpers_spec.rb
+++ b/spec/rubocop/code_reuse_helpers_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'parser/current'
require_relative '../../rubocop/code_reuse_helpers'
@@ -172,31 +172,6 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end
end
- describe '#in_graphql_types?' do
- %w[
- app/graphql/types
- ee/app/graphql/ee/types
- ee/app/graphql/types
- ].each do |path|
- it "returns true for a node in #{path}" do
- node = build_and_parse_source('10', rails_root_join(path, 'foo.rb'))
-
- expect(cop.in_graphql_types?(node)).to eq(true)
- end
- end
-
- %w[
- app/graphql/resolvers
- app/foo
- ].each do |path|
- it "returns false for a node in #{path}" do
- node = build_and_parse_source('10', rails_root_join(path, 'foo.rb'))
-
- expect(cop.in_graphql_types?(node)).to eq(false)
- end
- end
- end
-
describe '#in_api?' do
it 'returns true for a node in the API directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
@@ -367,7 +342,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
expect(cop)
.to receive(:add_offense)
- .with(send_node, location: :expression, message: 'oops')
+ .with(send_node, message: 'oops')
cop.disallow_send_to(def_node, 'Finder', 'oops')
end
diff --git a/spec/rubocop/cop/active_model_errors_direct_manipulation_spec.rb b/spec/rubocop/cop/active_model_errors_direct_manipulation_spec.rb
index 37fcdb38907..6be2f4945fd 100644
--- a/spec/rubocop/cop/active_model_errors_direct_manipulation_spec.rb
+++ b/spec/rubocop/cop/active_model_errors_direct_manipulation_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/active_model_errors_direct_manipulation'
RSpec.describe RuboCop::Cop::ActiveModelErrorsDirectManipulation do
- subject(:cop) { described_class.new }
-
context 'when modifying errors' do
it 'registers an offense' do
expect_offense(<<~PATTERN)
diff --git a/spec/rubocop/cop/active_record_association_reload_spec.rb b/spec/rubocop/cop/active_record_association_reload_spec.rb
index 1c0518815ee..9f101b80d9a 100644
--- a/spec/rubocop/cop/active_record_association_reload_spec.rb
+++ b/spec/rubocop/cop/active_record_association_reload_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/active_record_association_reload'
RSpec.describe RuboCop::Cop::ActiveRecordAssociationReload do
- subject(:cop) { described_class.new }
-
context 'when using ActiveRecord::Base' do
it 'registers an offense on reload usage' do
expect_offense(<<~PATTERN)
diff --git a/spec/rubocop/cop/api/base_spec.rb b/spec/rubocop/cop/api/base_spec.rb
index 547d3f53a08..66e99b75643 100644
--- a/spec/rubocop/cop/api/base_spec.rb
+++ b/spec/rubocop/cop/api/base_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/api/base'
RSpec.describe RuboCop::Cop::API::Base do
- subject(:cop) { described_class.new }
-
let(:corrected) do
<<~CORRECTED
class SomeAPI < ::API::Base
diff --git a/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
index 01f1fc71f9a..1d1754df838 100644
--- a/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
+++ b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/api/grape_array_missing_coerce'
RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
@@ -10,8 +10,6 @@ RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
"https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions"
end
- subject(:cop) { described_class.new }
-
it 'adds an offense with a required parameter' do
expect_offense(<<~TYPE)
class SomeAPI < Grape::API::Instance
diff --git a/spec/rubocop/cop/avoid_becomes_spec.rb b/spec/rubocop/cop/avoid_becomes_spec.rb
index 3ab1544b00d..d67b79329c9 100644
--- a/spec/rubocop/cop/avoid_becomes_spec.rb
+++ b/spec/rubocop/cop/avoid_becomes_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/avoid_becomes'
RSpec.describe RuboCop::Cop::AvoidBecomes do
- subject(:cop) { described_class.new }
-
it 'flags the use of becomes with a constant parameter' do
expect_offense(<<~CODE)
foo.becomes(Project)
diff --git a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
index cc851045c3c..9b7d988cb24 100644
--- a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
+++ b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/avoid_break_from_strong_memoize'
RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
- subject(:cop) { described_class.new }
-
it 'flags violation for break inside strong_memoize' do
expect_offense(<<~RUBY)
strong_memoize(:result) do
diff --git a/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
index 90ee5772b66..eb2417a7eef 100644
--- a/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
+++ b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers'
RSpec.describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
- subject(:cop) { described_class.new }
-
it 'flags violation for keyword arguments usage in perform method signature' do
expect_offense(<<~RUBY)
def perform(id:)
diff --git a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
index 86098f1afcc..e35705ae791 100644
--- a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
+++ b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/avoid_return_from_blocks'
RSpec.describe RuboCop::Cop::AvoidReturnFromBlocks do
- subject(:cop) { described_class.new }
-
it 'flags violation for return inside a block' do
expect_offense(<<~RUBY)
call do
diff --git a/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb b/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb
index 61d6f45b5ba..377050eb301 100644
--- a/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb
+++ b/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/avoid_route_redirect_leading_slash'
RSpec.describe RuboCop::Cop::AvoidRouteRedirectLeadingSlash do
- subject(:cop) { described_class.new }
-
before do
allow(cop).to receive(:in_routes?).and_return(true)
end
diff --git a/spec/rubocop/cop/ban_catch_throw_spec.rb b/spec/rubocop/cop/ban_catch_throw_spec.rb
index f255d27e7c7..a41868410eb 100644
--- a/spec/rubocop/cop/ban_catch_throw_spec.rb
+++ b/spec/rubocop/cop/ban_catch_throw_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/ban_catch_throw'
RSpec.describe RuboCop::Cop::BanCatchThrow do
- subject(:cop) { described_class.new }
-
it 'registers an offense when `catch` or `throw` are used' do
expect_offense(<<~CODE)
catch(:foo) {
diff --git a/spec/rubocop/cop/code_reuse/finder_spec.rb b/spec/rubocop/cop/code_reuse/finder_spec.rb
index 36f44ca79da..8e285e3d988 100644
--- a/spec/rubocop/cop/code_reuse/finder_spec.rb
+++ b/spec/rubocop/cop/code_reuse/finder_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/code_reuse/finder'
RSpec.describe RuboCop::Cop::CodeReuse::Finder do
- subject(:cop) { described_class.new }
-
it 'flags the use of a Finder inside another Finder' do
allow(cop)
.to receive(:in_finder?)
diff --git a/spec/rubocop/cop/code_reuse/presenter_spec.rb b/spec/rubocop/cop/code_reuse/presenter_spec.rb
index 070a7ed760c..fb7a95f930d 100644
--- a/spec/rubocop/cop/code_reuse/presenter_spec.rb
+++ b/spec/rubocop/cop/code_reuse/presenter_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/code_reuse/presenter'
RSpec.describe RuboCop::Cop::CodeReuse::Presenter do
- subject(:cop) { described_class.new }
-
it 'flags the use of a Presenter in a Service class' do
allow(cop)
.to receive(:in_service_class?)
diff --git a/spec/rubocop/cop/code_reuse/serializer_spec.rb b/spec/rubocop/cop/code_reuse/serializer_spec.rb
index d5577caa2b4..b1f22c7b969 100644
--- a/spec/rubocop/cop/code_reuse/serializer_spec.rb
+++ b/spec/rubocop/cop/code_reuse/serializer_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/code_reuse/serializer'
RSpec.describe RuboCop::Cop::CodeReuse::Serializer do
- subject(:cop) { described_class.new }
-
it 'flags the use of a Serializer in a Service class' do
allow(cop)
.to receive(:in_service_class?)
diff --git a/spec/rubocop/cop/code_reuse/service_class_spec.rb b/spec/rubocop/cop/code_reuse/service_class_spec.rb
index 353225b2c42..5792b86a535 100644
--- a/spec/rubocop/cop/code_reuse/service_class_spec.rb
+++ b/spec/rubocop/cop/code_reuse/service_class_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/code_reuse/service_class'
RSpec.describe RuboCop::Cop::CodeReuse::ServiceClass do
- subject(:cop) { described_class.new }
-
it 'flags the use of a Service class in a Finder' do
allow(cop)
.to receive(:in_finder?)
diff --git a/spec/rubocop/cop/code_reuse/worker_spec.rb b/spec/rubocop/cop/code_reuse/worker_spec.rb
index a548e90d8e1..2df5ebc56fa 100644
--- a/spec/rubocop/cop/code_reuse/worker_spec.rb
+++ b/spec/rubocop/cop/code_reuse/worker_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/code_reuse/worker'
RSpec.describe RuboCop::Cop::CodeReuse::Worker do
- subject(:cop) { described_class.new }
-
it 'flags the use of a worker in a controller' do
allow(cop)
.to receive(:in_controller?)
diff --git a/spec/rubocop/cop/database/disable_referential_integrity_spec.rb b/spec/rubocop/cop/database/disable_referential_integrity_spec.rb
index 9ac67363cb6..5d31e55e586 100644
--- a/spec/rubocop/cop/database/disable_referential_integrity_spec.rb
+++ b/spec/rubocop/cop/database/disable_referential_integrity_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/database/disable_referential_integrity'
RSpec.describe RuboCop::Cop::Database::DisableReferentialIntegrity do
- subject(:cop) { described_class.new }
-
it 'does not flag the use of disable_referential_integrity with a send receiver' do
expect_offense(<<~SOURCE)
foo.disable_referential_integrity
diff --git a/spec/rubocop/cop/database/establish_connection_spec.rb b/spec/rubocop/cop/database/establish_connection_spec.rb
index 3919872b5e7..987f68def75 100644
--- a/spec/rubocop/cop/database/establish_connection_spec.rb
+++ b/spec/rubocop/cop/database/establish_connection_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/database/establish_connection'
RSpec.describe RuboCop::Cop::Database::EstablishConnection do
- subject(:cop) { described_class.new }
-
it 'flags the use of ActiveRecord::Base.establish_connection' do
expect_offense(<<~CODE)
ActiveRecord::Base.establish_connection
diff --git a/spec/rubocop/cop/database/multiple_databases_spec.rb b/spec/rubocop/cop/database/multiple_databases_spec.rb
index 6ee1e7b13ca..124e8aaf39c 100644
--- a/spec/rubocop/cop/database/multiple_databases_spec.rb
+++ b/spec/rubocop/cop/database/multiple_databases_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/database/multiple_databases'
RSpec.describe RuboCop::Cop::Database::MultipleDatabases do
- subject(:cop) { described_class.new }
-
it 'flags the use of ActiveRecord::Base.connection' do
expect_offense(<<~SOURCE)
ActiveRecord::Base.connection.inspect
diff --git a/spec/rubocop/cop/database/rescue_query_canceled_spec.rb b/spec/rubocop/cop/database/rescue_query_canceled_spec.rb
index 56314a18bf5..2418b45e3bb 100644
--- a/spec/rubocop/cop/database/rescue_query_canceled_spec.rb
+++ b/spec/rubocop/cop/database/rescue_query_canceled_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/database/rescue_query_canceled'
RSpec.describe RuboCop::Cop::Database::RescueQueryCanceled do
- subject(:cop) { described_class.new }
-
it 'flags the use of ActiveRecord::QueryCanceled' do
expect_offense(<<~CODE)
begin
diff --git a/spec/rubocop/cop/database/rescue_statement_timeout_spec.rb b/spec/rubocop/cop/database/rescue_statement_timeout_spec.rb
index b9b2ce1c16b..bfeba2b4d00 100644
--- a/spec/rubocop/cop/database/rescue_statement_timeout_spec.rb
+++ b/spec/rubocop/cop/database/rescue_statement_timeout_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/database/rescue_statement_timeout'
RSpec.describe RuboCop::Cop::Database::RescueStatementTimeout do
- subject(:cop) { described_class.new }
-
it 'flags the use of ActiveRecord::StatementTimeout' do
expect_offense(<<~CODE)
begin
diff --git a/spec/rubocop/cop/default_scope_spec.rb b/spec/rubocop/cop/default_scope_spec.rb
index 4fac0d465e0..d1f26cdce46 100644
--- a/spec/rubocop/cop/default_scope_spec.rb
+++ b/spec/rubocop/cop/default_scope_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/default_scope'
RSpec.describe RuboCop::Cop::DefaultScope do
- subject(:cop) { described_class.new }
-
it 'does not flag the use of default_scope with a send receiver' do
expect_no_offenses('foo.default_scope')
end
diff --git a/spec/rubocop/cop/destroy_all_spec.rb b/spec/rubocop/cop/destroy_all_spec.rb
index 468b10c3816..f984822a4e8 100644
--- a/spec/rubocop/cop/destroy_all_spec.rb
+++ b/spec/rubocop/cop/destroy_all_spec.rb
@@ -1,21 +1,19 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/destroy_all'
RSpec.describe RuboCop::Cop::DestroyAll do
- subject(:cop) { described_class.new }
-
it 'flags the use of destroy_all with a send receiver' do
expect_offense(<<~CODE)
- foo.destroy_all # rubocop: disable Cop/DestroyAll
+ foo.destroy_all
^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
CODE
end
it 'flags the use of destroy_all with a constant receiver' do
expect_offense(<<~CODE)
- User.destroy_all # rubocop: disable Cop/DestroyAll
+ User.destroy_all
^^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
CODE
end
@@ -30,7 +28,7 @@ RSpec.describe RuboCop::Cop::DestroyAll do
it 'flags the use of destroy_all with a local variable receiver' do
expect_offense(<<~CODE)
users = User.all
- users.destroy_all # rubocop: disable Cop/DestroyAll
+ users.destroy_all
^^^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
CODE
end
diff --git a/spec/rubocop/cop/file_decompression_spec.rb b/spec/rubocop/cop/file_decompression_spec.rb
index 7be1a784001..19d71a2e85b 100644
--- a/spec/rubocop/cop/file_decompression_spec.rb
+++ b/spec/rubocop/cop/file_decompression_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/file_decompression'
RSpec.describe RuboCop::Cop::FileDecompression do
- subject(:cop) { described_class.new }
-
it 'does not flag when using a system command not related to file decompression' do
expect_no_offenses('system("ls")')
end
diff --git a/spec/rubocop/cop/filename_length_spec.rb b/spec/rubocop/cop/filename_length_spec.rb
index ee128cb2781..1ea368d282f 100644
--- a/spec/rubocop/cop/filename_length_spec.rb
+++ b/spec/rubocop/cop/filename_length_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/filename_length'
RSpec.describe RuboCop::Cop::FilenameLength do
- subject(:cop) { described_class.new }
-
it 'does not flag files with names 100 characters long' do
expect_no_offenses('puts "it does not matter"', 'a' * 100)
end
diff --git a/spec/rubocop/cop/gemspec/avoid_executing_git_spec.rb b/spec/rubocop/cop/gemspec/avoid_executing_git_spec.rb
index f94a990a2f7..6a1982d8101 100644
--- a/spec/rubocop/cop/gemspec/avoid_executing_git_spec.rb
+++ b/spec/rubocop/cop/gemspec/avoid_executing_git_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gemspec/avoid_executing_git'
RSpec.describe RuboCop::Cop::Gemspec::AvoidExecutingGit do
- subject(:cop) { described_class.new }
-
it 'flags violation for executing git' do
expect_offense(<<~RUBY)
Gem::Specification.new do |gem|
diff --git a/spec/rubocop/cop/gitlab/avoid_feature_category_not_owned_spec.rb b/spec/rubocop/cop/gitlab/avoid_feature_category_not_owned_spec.rb
index f6c6955f6bb..9cacee5f75d 100644
--- a/spec/rubocop/cop/gitlab/avoid_feature_category_not_owned_spec.rb
+++ b/spec/rubocop/cop/gitlab/avoid_feature_category_not_owned_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/avoid_feature_category_not_owned'
RSpec.describe RuboCop::Cop::Gitlab::AvoidFeatureCategoryNotOwned do
- subject(:cop) { described_class.new }
-
shared_examples 'defining feature category on a class' do
it 'flags a method call on a class' do
expect_offense(<<~SOURCE)
@@ -31,7 +29,7 @@ RSpec.describe RuboCop::Cop::Gitlab::AvoidFeatureCategoryNotOwned do
context 'in controllers' do
before do
- allow(subject).to receive(:in_controller?).and_return(true)
+ allow(cop).to receive(:in_controller?).and_return(true)
end
it_behaves_like 'defining feature category on a class'
@@ -39,7 +37,7 @@ RSpec.describe RuboCop::Cop::Gitlab::AvoidFeatureCategoryNotOwned do
context 'in workers' do
before do
- allow(subject).to receive(:in_worker?).and_return(true)
+ allow(cop).to receive(:in_worker?).and_return(true)
end
it_behaves_like 'defining feature category on a class'
@@ -47,7 +45,7 @@ RSpec.describe RuboCop::Cop::Gitlab::AvoidFeatureCategoryNotOwned do
context 'for grape endpoints' do
before do
- allow(subject).to receive(:in_api?).and_return(true)
+ allow(cop).to receive(:in_api?).and_return(true)
end
it_behaves_like 'defining feature category on a class'
diff --git a/spec/rubocop/cop/gitlab/avoid_feature_get_spec.rb b/spec/rubocop/cop/gitlab/avoid_feature_get_spec.rb
new file mode 100644
index 00000000000..b5017bebd28
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/avoid_feature_get_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/gitlab/avoid_feature_get'
+
+RSpec.describe RuboCop::Cop::Gitlab::AvoidFeatureGet do
+ let(:msg) { described_class::MSG }
+
+ subject(:cop) { described_class.new }
+
+ it 'bans use of Feature.ban' do
+ expect_offense(<<~RUBY)
+ Feature.get
+ ^^^ #{msg}
+ Feature.get(x)
+ ^^^ #{msg}
+ ::Feature.get
+ ^^^ #{msg}
+ ::Feature.get(x)
+ ^^^ #{msg}
+ RUBY
+ end
+
+ it 'ignores unrelated code' do
+ expect_no_offenses(<<~RUBY)
+ Namespace::Feature.get
+ Namespace::Feature.get(x)
+ Feature.remove(:x)
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb b/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb
index 6d69eb5456f..09d5552d40c 100644
--- a/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb
+++ b/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/avoid_uploaded_file_from_params'
RSpec.describe RuboCop::Cop::Gitlab::AvoidUploadedFileFromParams do
- subject(:cop) { described_class.new }
-
context 'when using UploadedFile.from_params' do
it 'flags its call' do
expect_offense(<<~SOURCE)
diff --git a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
index 7cd003d0a70..28fdd18b0f5 100644
--- a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
+++ b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/bulk_insert'
RSpec.describe RuboCop::Cop::Gitlab::BulkInsert do
- subject(:cop) { described_class.new }
-
it 'flags the use of ApplicationRecord.legacy_bulk_insert' do
expect_offense(<<~SOURCE)
ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows)
diff --git a/spec/rubocop/cop/gitlab/change_timezone_spec.rb b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
index ff6365aa0f7..d5100cb662a 100644
--- a/spec/rubocop/cop/gitlab/change_timezone_spec.rb
+++ b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/change_timezone'
RSpec.describe RuboCop::Cop::Gitlab::ChangeTimezone do
- subject(:cop) { described_class.new }
-
context 'Time.zone=' do
it 'registers an offense with no 2nd argument' do
expect_offense(<<~PATTERN)
diff --git a/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
index 1d99ec93e25..99cc9e0b469 100644
--- a/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
+++ b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/const_get_inherit_false'
RSpec.describe RuboCop::Cop::Gitlab::ConstGetInheritFalse do
- subject(:cop) { described_class.new }
-
context 'Object.const_get' do
it 'registers an offense with no 2nd argument and corrects' do
expect_offense(<<~PATTERN)
diff --git a/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb b/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb
index 1ceff0dd681..1b497954aee 100644
--- a/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb
+++ b/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/delegate_predicate_methods'
RSpec.describe RuboCop::Cop::Gitlab::DelegatePredicateMethods do
- subject(:cop) { described_class.new }
-
it 'registers offense for single predicate method with allow_nil:true' do
expect_offense(<<~SOURCE)
delegate :is_foo?, :do_foo, to: :bar, allow_nil: true
diff --git a/spec/rubocop/cop/gitlab/deprecate_track_redis_hll_event_spec.rb b/spec/rubocop/cop/gitlab/deprecate_track_redis_hll_event_spec.rb
index 453f0c36c14..eed30e11a98 100644
--- a/spec/rubocop/cop/gitlab/deprecate_track_redis_hll_event_spec.rb
+++ b/spec/rubocop/cop/gitlab/deprecate_track_redis_hll_event_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/deprecate_track_redis_hll_event'
RSpec.describe RuboCop::Cop::Gitlab::DeprecateTrackRedisHLLEvent do
- subject(:cop) { described_class.new }
-
it 'does not flag the use of track_event' do
expect_no_offenses('track_event :show, name: "p_analytics_insights"')
end
diff --git a/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb b/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
index 3b3d5b01a30..9a1639806c8 100644
--- a/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
+++ b/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/duplicate_spec_location'
RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation do
- subject(:cop) { described_class.new }
-
let(:rails_root) { '../../../../' }
def full_path(path)
diff --git a/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb b/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb
index e17fb71f9bc..7c692d5aad4 100644
--- a/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb
+++ b/spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/event_store_subscriber'
RSpec.describe RuboCop::Cop::Gitlab::EventStoreSubscriber do
- subject(:cop) { described_class.new }
-
context 'when an event store subscriber overrides #perform' do
it 'registers an offense' do
expect_offense(<<~WORKER)
diff --git a/spec/rubocop/cop/gitlab/except_spec.rb b/spec/rubocop/cop/gitlab/except_spec.rb
index 04cfe261cf2..47048b8f658 100644
--- a/spec/rubocop/cop/gitlab/except_spec.rb
+++ b/spec/rubocop/cop/gitlab/except_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/except'
RSpec.describe RuboCop::Cop::Gitlab::Except do
- subject(:cop) { described_class.new }
-
it 'flags the use of Gitlab::SQL::Except.new' do
expect_offense(<<~SOURCE)
Gitlab::SQL::Except.new([foo])
diff --git a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
index 514ef357785..30edd33a318 100644
--- a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
+++ b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/feature_available_usage'
RSpec.describe RuboCop::Cop::Gitlab::FeatureAvailableUsage do
- subject(:cop) { described_class.new }
-
context 'no arguments given' do
it 'does not flag the use of Gitlab::Sourcegraph.feature_available? with no arguments' do
expect_no_offenses('Gitlab::Sourcegraph.feature_available?')
diff --git a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
index d2cd06d77c5..6e01ef1bdec 100644
--- a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
+++ b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/finder_with_find_by'
RSpec.describe RuboCop::Cop::Gitlab::FinderWithFindBy do
- subject(:cop) { described_class.new }
-
context 'when calling execute.find' do
it 'registers an offense and corrects' do
expect_offense(<<~CODE)
diff --git a/spec/rubocop/cop/gitlab/httparty_spec.rb b/spec/rubocop/cop/gitlab/httparty_spec.rb
index 98b1aa36586..09204009d9b 100644
--- a/spec/rubocop/cop/gitlab/httparty_spec.rb
+++ b/spec/rubocop/cop/gitlab/httparty_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/httparty'
RSpec.describe RuboCop::Cop::Gitlab::HTTParty do # rubocop:disable RSpec/FilePath
- subject(:cop) { described_class.new }
-
shared_examples('registering include offense') do
it 'registers an offense when the class includes HTTParty' do
expect_offense(source)
diff --git a/spec/rubocop/cop/gitlab/intersect_spec.rb b/spec/rubocop/cop/gitlab/intersect_spec.rb
index f3cb1412f35..c81dae9af97 100644
--- a/spec/rubocop/cop/gitlab/intersect_spec.rb
+++ b/spec/rubocop/cop/gitlab/intersect_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/intersect'
RSpec.describe RuboCop::Cop::Gitlab::Intersect do
- subject(:cop) { described_class.new }
-
it 'flags the use of Gitlab::SQL::Intersect.new' do
expect_offense(<<~SOURCE)
Gitlab::SQL::Intersect.new([foo])
diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb
index 7998f26da4e..e4ec107747d 100644
--- a/spec/rubocop/cop/gitlab/json_spec.rb
+++ b/spec/rubocop/cop/gitlab/json_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/json'
RSpec.describe RuboCop::Cop::Gitlab::Json do
- subject(:cop) { described_class.new }
-
context 'when ::JSON is called' do
it 'registers an offense' do
expect_offense(<<~RUBY)
diff --git a/spec/rubocop/cop/gitlab/keys_first_and_values_first_spec.rb b/spec/rubocop/cop/gitlab/keys_first_and_values_first_spec.rb
new file mode 100644
index 00000000000..073c78e78c0
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/keys_first_and_values_first_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/gitlab/keys_first_and_values_first'
+
+RSpec.describe RuboCop::Cop::Gitlab::KeysFirstAndValuesFirst do
+ let(:msg) { described_class::MSG }
+
+ subject(:cop) { described_class.new }
+
+ shared_examples 'inspect use of keys or values first' do |method, autocorrect|
+ describe ".#{method}.first" do
+ it 'flags and autocorrects' do
+ expect_offense(<<~RUBY, method: method, autocorrect: autocorrect)
+ hash.%{method}.first
+ _{method} ^^^^^ Prefer `.%{autocorrect}.first` over `.%{method}.first`. [...]
+ var = {a: 1}; var.%{method}.first
+ _{method} ^^^^^ Prefer `.%{autocorrect}.first` over `.%{method}.first`. [...]
+ {a: 1}.%{method}.first
+ _{method} ^^^^^ Prefer `.%{autocorrect}.first` over `.%{method}.first`. [...]
+ CONST.%{method}.first
+ _{method} ^^^^^ Prefer `.%{autocorrect}.first` over `.%{method}.first`. [...]
+ ::CONST.%{method}.first
+ _{method} ^^^^^ Prefer `.%{autocorrect}.first` over `.%{method}.first`. [...]
+ RUBY
+
+ expect_correction(<<~RUBY)
+ hash.#{autocorrect}.first
+ var = {a: 1}; var.#{autocorrect}.first
+ {a: 1}.#{autocorrect}.first
+ CONST.#{autocorrect}.first
+ ::CONST.#{autocorrect}.first
+ RUBY
+ end
+
+ it 'does not flag unrelated code' do
+ expect_no_offenses(<<~RUBY)
+ array.first
+ hash.#{method}.last
+ hash.#{method}
+ #{method}.first
+ 1.#{method}.first
+ 'string'.#{method}.first
+ RUBY
+ end
+ end
+ end
+
+ it_behaves_like 'inspect use of keys or values first', :keys, :each_key
+ it_behaves_like 'inspect use of keys or values first', :values, :each_value
+end
diff --git a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
index 9ab5cdc24a4..ac7e41dda44 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/mark_used_feature_flags'
@@ -10,8 +10,6 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
%w[a_feature_flag foo_hello foo_world baz_experiment_percentage bar_baz]
end
- subject(:cop) { described_class.new }
-
before do
allow(cop).to receive(:defined_feature_flags).and_return(defined_feature_flags)
allow(cop).to receive(:usage_data_counters_known_event_feature_flags).and_return([])
@@ -48,6 +46,7 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
Feature.enabled?
Feature.disabled?
push_frontend_feature_flag
+ YamlProcessor::FeatureFlags.enabled?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
@@ -212,19 +211,6 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
include_examples 'does not set any flags as used', 'deduplicate :delayed'
end
- describe 'GraphQL `field` method' do
- before do
- allow(cop).to receive(:in_graphql_types?).and_return(true)
- end
-
- include_examples 'sets flag as used', 'field :runners, Types::Ci::RunnerType.connection_type, null: true, _deprecated_feature_flag: :foo', 'foo'
- include_examples 'sets flag as used', 'field :runners, null: true, _deprecated_feature_flag: :foo', 'foo'
- include_examples 'does not set any flags as used', 'field :solution'
- include_examples 'does not set any flags as used', 'field :runners, Types::Ci::RunnerType.connection_type'
- include_examples 'does not set any flags as used', 'field :runners, Types::Ci::RunnerType.connection_type, null: true, description: "hello world"'
- include_examples 'does not set any flags as used', 'field :solution, type: GraphQL::Types::String, null: true, description: "URL to the vulnerabilitys details page."'
- end
-
describe "tracking of usage data metrics known events happens at the beginning of inspection" do
let(:usage_data_counters_known_event_feature_flags) { ['an_event_feature_flag'] }
diff --git a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
index d46dec3b2e3..9f1691696eb 100644
--- a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
+++ b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/module_with_instance_variables'
RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
let(:msg) { "Do not use instance variables in a module. [...]" }
- subject(:cop) { described_class.new }
-
shared_examples('registering offense') do
it 'registers an offense when instance variable is used in a module' do
expect_offense(source)
diff --git a/spec/rubocop/cop/gitlab/namespaced_class_spec.rb b/spec/rubocop/cop/gitlab/namespaced_class_spec.rb
index 83d0eaf4884..b16c3aba5c7 100644
--- a/spec/rubocop/cop/gitlab/namespaced_class_spec.rb
+++ b/spec/rubocop/cop/gitlab/namespaced_class_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/namespaced_class'
RSpec.describe RuboCop::Cop::Gitlab::NamespacedClass do
- subject(:cop) { described_class.new }
-
shared_examples 'enforces namespaced classes' do
def namespaced(code)
return code unless namespace
diff --git a/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb b/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb
index f73fc71b601..d00a9861c77 100644
--- a/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb
+++ b/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/policy_rule_boolean'
RSpec.describe RuboCop::Cop::Gitlab::PolicyRuleBoolean do
- subject(:cop) { described_class.new }
-
it 'registers offense for &&' do
expect_offense(<<~SOURCE)
rule { conducts_electricity && batteries }.enable :light_bulb
diff --git a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
index 903c02ba194..1ca34ad90da 100644
--- a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
+++ b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/predicate_memoization'
RSpec.describe RuboCop::Cop::Gitlab::PredicateMemoization do
- subject(:cop) { described_class.new }
-
shared_examples('not registering offense') do
it 'does not register offenses' do
expect_no_offenses(source)
diff --git a/spec/rubocop/cop/gitlab/rails_logger_spec.rb b/spec/rubocop/cop/gitlab/rails_logger_spec.rb
index 24f49bf3044..c9d361b49b8 100644
--- a/spec/rubocop/cop/gitlab/rails_logger_spec.rb
+++ b/spec/rubocop/cop/gitlab/rails_logger_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/rails_logger'
RSpec.describe RuboCop::Cop::Gitlab::RailsLogger do
- subject(:cop) { described_class.new }
-
described_class::LOG_METHODS.each do |method|
it "flags the use of Rails.logger.#{method} with a constant receiver" do
node = "Rails.logger.#{method}('some error')"
diff --git a/spec/rubocop/cop/gitlab/union_spec.rb b/spec/rubocop/cop/gitlab/union_spec.rb
index ce84c75338d..4042fe0263a 100644
--- a/spec/rubocop/cop/gitlab/union_spec.rb
+++ b/spec/rubocop/cop/gitlab/union_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/union'
RSpec.describe RuboCop::Cop::Gitlab::Union do
- subject(:cop) { described_class.new }
-
it 'flags the use of Gitlab::SQL::Union.new' do
expect_offense(<<~SOURCE)
Gitlab::SQL::Union.new([foo])
diff --git a/spec/rubocop/cop/graphql/authorize_types_spec.rb b/spec/rubocop/cop/graphql/authorize_types_spec.rb
index 7aa36030526..a30cd5a1688 100644
--- a/spec/rubocop/cop/graphql/authorize_types_spec.rb
+++ b/spec/rubocop/cop/graphql/authorize_types_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/authorize_types'
RSpec.describe RuboCop::Cop::Graphql::AuthorizeTypes do
- subject(:cop) { described_class.new }
-
it 'adds an offense when there is no authorize call' do
expect_offense(<<~TYPE)
module Types
diff --git a/spec/rubocop/cop/graphql/descriptions_spec.rb b/spec/rubocop/cop/graphql/descriptions_spec.rb
index 84520a89b08..8826e700fdf 100644
--- a/spec/rubocop/cop/graphql/descriptions_spec.rb
+++ b/spec/rubocop/cop/graphql/descriptions_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/descriptions'
RSpec.describe RuboCop::Cop::Graphql::Descriptions do
- subject(:cop) { described_class.new }
-
context 'with fields' do
it 'adds an offense when there is no description' do
expect_offense(<<~TYPE)
diff --git a/spec/rubocop/cop/graphql/gid_expected_type_spec.rb b/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
index 47a6ce24d53..563c16a99df 100644
--- a/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
+++ b/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/gid_expected_type'
RSpec.describe RuboCop::Cop::Graphql::GIDExpectedType do
- subject(:cop) { described_class.new }
-
it 'adds an offense when there is no expected_type parameter' do
expect_offense(<<~TYPE)
GitlabSchema.object_from_id(received_id)
diff --git a/spec/rubocop/cop/graphql/graphql_name_position_spec.rb b/spec/rubocop/cop/graphql/graphql_name_position_spec.rb
index 42cc398ed84..5db6fe6a801 100644
--- a/spec/rubocop/cop/graphql/graphql_name_position_spec.rb
+++ b/spec/rubocop/cop/graphql/graphql_name_position_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/graphql_name_position'
RSpec.describe RuboCop::Cop::Graphql::GraphqlNamePosition do
- subject(:cop) { described_class.new }
-
it 'adds an offense when graphql_name is not on the first line' do
expect_offense(<<~TYPE)
module Types
diff --git a/spec/rubocop/cop/graphql/id_type_spec.rb b/spec/rubocop/cop/graphql/id_type_spec.rb
index d71031c6e1a..3a56753d39e 100644
--- a/spec/rubocop/cop/graphql/id_type_spec.rb
+++ b/spec/rubocop/cop/graphql/id_type_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/id_type'
RSpec.describe RuboCop::Cop::Graphql::IDType do
- subject(:cop) { described_class.new }
-
it 'adds an offense when GraphQL::Types::ID is used as a param to #argument' do
expect_offense(<<~TYPE)
argument :some_arg, GraphQL::Types::ID, some: other, params: do_not_matter
diff --git a/spec/rubocop/cop/graphql/json_type_spec.rb b/spec/rubocop/cop/graphql/json_type_spec.rb
index 882e2b2ef88..c72e5b5b1c9 100644
--- a/spec/rubocop/cop/graphql/json_type_spec.rb
+++ b/spec/rubocop/cop/graphql/json_type_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/json_type'
RSpec.describe RuboCop::Cop::Graphql::JSONType do
@@ -8,8 +8,6 @@ RSpec.describe RuboCop::Cop::Graphql::JSONType do
'Avoid using GraphQL::Types::JSON. See: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#json'
end
- subject(:cop) { described_class.new }
-
context 'fields' do
it 'adds an offense when GraphQL::Types::JSON is used' do
expect_offense(<<~RUBY)
diff --git a/spec/rubocop/cop/graphql/old_types_spec.rb b/spec/rubocop/cop/graphql/old_types_spec.rb
index 5cf3b11548f..45d47f3b516 100644
--- a/spec/rubocop/cop/graphql/old_types_spec.rb
+++ b/spec/rubocop/cop/graphql/old_types_spec.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rspec-parameterized'
require_relative '../../../../rubocop/cop/graphql/old_types'
RSpec.describe RuboCop::Cop::Graphql::OldTypes do
using RSpec::Parameterized::TableSyntax
- subject(:cop) { described_class.new }
-
where(:old_type, :message) do
'GraphQL::ID_TYPE' | 'Avoid using GraphQL::ID_TYPE. Use GraphQL::Types::ID instead'
'GraphQL::INT_TYPE' | 'Avoid using GraphQL::INT_TYPE. Use GraphQL::Types::Int instead'
diff --git a/spec/rubocop/cop/graphql/resolver_type_spec.rb b/spec/rubocop/cop/graphql/resolver_type_spec.rb
index 06bf90a8a07..ade1bfc2cca 100644
--- a/spec/rubocop/cop/graphql/resolver_type_spec.rb
+++ b/spec/rubocop/cop/graphql/resolver_type_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/resolver_type'
RSpec.describe RuboCop::Cop::Graphql::ResolverType do
- subject(:cop) { described_class.new }
-
it 'adds an offense when there is no type annotation' do
expect_offense(<<~SRC)
module Resolvers
diff --git a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
index 2348552f9e4..c948ea606b8 100644
--- a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
+++ b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/group_public_or_visible_to_user'
RSpec.describe RuboCop::Cop::GroupPublicOrVisibleToUser do
@@ -9,8 +9,6 @@ RSpec.describe RuboCop::Cop::GroupPublicOrVisibleToUser do
"Please ensure that you are not using it on its own and that the amount of rows being filtered is reasonable."
end
- subject(:cop) { described_class.new }
-
it 'flags the use of Group.public_or_visible_to_user with a constant receiver' do
expect_offense(<<~CODE)
Group.public_or_visible_to_user
diff --git a/spec/rubocop/cop/ignored_columns_spec.rb b/spec/rubocop/cop/ignored_columns_spec.rb
index f87b1a1e520..c6c44399624 100644
--- a/spec/rubocop/cop/ignored_columns_spec.rb
+++ b/spec/rubocop/cop/ignored_columns_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/ignored_columns'
RSpec.describe RuboCop::Cop::IgnoredColumns do
- subject(:cop) { described_class.new }
-
it 'flags direct use of ignored_columns instead of the IgnoredColumns concern' do
expect_offense(<<~RUBY)
class Foo < ApplicationRecord
diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
index 8c706925ab9..f86bb1427db 100644
--- a/spec/rubocop/cop/include_sidekiq_worker_spec.rb
+++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/include_sidekiq_worker'
RSpec.describe RuboCop::Cop::IncludeSidekiqWorker do
- subject(:cop) { described_class.new }
-
context 'when `Sidekiq::Worker` is included' do
it 'registers an offense and corrects', :aggregate_failures do
expect_offense(<<~CODE)
diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
index 3596badc599..3063a474bd7 100644
--- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
+++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/inject_enterprise_edition_module'
RSpec.describe RuboCop::Cop::InjectEnterpriseEditionModule do
- subject(:cop) { described_class.new }
-
it 'flags the use of `prepend_mod_with` in the middle of a file' do
expect_offense(<<~SOURCE)
class Foo
diff --git a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
index b1b4c88e0f6..b0551a79c50 100644
--- a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
+++ b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
@@ -1,19 +1,18 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/lint/last_keyword_argument'
RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument do
- subject(:cop) { described_class.new }
-
before do
described_class.instance_variable_set(:@keyword_warnings, nil)
+ allow(Dir).to receive(:glob).and_call_original
+ allow(File).to receive(:read).and_call_original
end
context 'deprecation files does not exist' do
before do
- allow(Dir).to receive(:glob).and_return([])
- allow(File).to receive(:exist?).and_return(false)
+ allow(Dir).to receive(:glob).with(described_class::DEPRECATIONS_GLOB).and_return([])
end
it 'does not register an offense' do
@@ -58,7 +57,8 @@ RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument do
before do
allow(Dir).to receive(:glob).and_return(['deprecations/service/create_spec.yml', 'deprecations/api/projects_spec.yml'])
- allow(File).to receive(:read).and_return(create_spec_yaml, projects_spec_yaml)
+ allow(File).to receive(:read).with('deprecations/service/create_spec.yml').and_return(create_spec_yaml)
+ allow(File).to receive(:read).with('deprecations/api/projects_spec.yml').and_return(projects_spec_yaml)
end
it 'registers an offense for last keyword warning' do
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
index 3f47613280f..865f567db44 100644
--- a/spec/rubocop/cop/migration/add_column_with_default_spec.rb
+++ b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_column_with_default'
RSpec.describe RuboCop::Cop::Migration::AddColumnWithDefault do
diff --git a/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb b/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb
index b78ec971245..7cc88946cf1 100644
--- a/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb
+++ b/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_columns_to_wide_tables'
RSpec.describe RuboCop::Cop::Migration::AddColumnsToWideTables do
diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
index 572c0d414b3..aa39f5f1603 100644
--- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key'
RSpec.describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
index 52b3a5769ff..185b64b0334 100644
--- a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_concurrent_index'
RSpec.describe RuboCop::Cop::Migration::AddConcurrentIndex do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/add_index_spec.rb b/spec/rubocop/cop/migration/add_index_spec.rb
index 088bfe434f4..338dbf73a3a 100644
--- a/spec/rubocop/cop/migration/add_index_spec.rb
+++ b/spec/rubocop/cop/migration/add_index_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_index'
RSpec.describe RuboCop::Cop::Migration::AddIndex do
- subject(:cop) { described_class.new }
-
context 'in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
index f6bed0d74fb..85a86a27c48 100644
--- a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
+++ b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_limit_to_text_columns'
RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
let(:msg) { 'Text columns should always have a limit set (255 is suggested)[...]' }
diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb
index 9445780e9ed..bb3fe7068b4 100644
--- a/spec/rubocop/cop/migration/add_reference_spec.rb
+++ b/spec/rubocop/cop/migration/add_reference_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_reference'
RSpec.describe RuboCop::Cop::Migration::AddReference do
diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb
index 2a11d46be6e..fcc2f4aa363 100644
--- a/spec/rubocop/cop/migration/add_timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/add_timestamps'
RSpec.describe RuboCop::Cop::Migration::AddTimestamps do
- subject(:cop) { described_class.new }
-
let(:migration_with_add_timestamps) do
%q(
class Users < ActiveRecord::Migration[4.2]
diff --git a/spec/rubocop/cop/migration/background_migration_base_class_spec.rb b/spec/rubocop/cop/migration/background_migration_base_class_spec.rb
index 0a110418139..8cc85ac692c 100644
--- a/spec/rubocop/cop/migration/background_migration_base_class_spec.rb
+++ b/spec/rubocop/cop/migration/background_migration_base_class_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/background_migration_base_class'
RSpec.describe RuboCop::Cop::Migration::BackgroundMigrationBaseClass do
- subject(:cop) { described_class.new }
-
context 'when the migration class inherits from BatchedMigrationJob' do
it 'does not register any offenses' do
expect_no_offenses(<<~RUBY)
diff --git a/spec/rubocop/cop/migration/background_migration_record_spec.rb b/spec/rubocop/cop/migration/background_migration_record_spec.rb
index b5724ef1efd..d5a451e00c9 100644
--- a/spec/rubocop/cop/migration/background_migration_record_spec.rb
+++ b/spec/rubocop/cop/migration/background_migration_record_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/background_migration_record'
RSpec.describe RuboCop::Cop::Migration::BackgroundMigrationRecord do
- subject(:cop) { described_class.new }
-
context 'outside of a migration' do
it 'does not register any offenses' do
expect_no_offenses(<<~SOURCE)
diff --git a/spec/rubocop/cop/migration/background_migrations_spec.rb b/spec/rubocop/cop/migration/background_migrations_spec.rb
index 3242211ab47..681bbd84562 100644
--- a/spec/rubocop/cop/migration/background_migrations_spec.rb
+++ b/spec/rubocop/cop/migration/background_migrations_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/background_migrations'
RSpec.describe RuboCop::Cop::Migration::BackgroundMigrations do
diff --git a/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb b/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb
index ac814c10550..7329d399330 100644
--- a/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb
+++ b/spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
#
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/complex_indexes_require_name'
RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
let(:msg) { 'indexes added with custom options must be explicitly named' }
diff --git a/spec/rubocop/cop/migration/create_table_with_foreign_keys_spec.rb b/spec/rubocop/cop/migration/create_table_with_foreign_keys_spec.rb
index 6a8df2b507d..072edb5827b 100644
--- a/spec/rubocop/cop/migration/create_table_with_foreign_keys_spec.rb
+++ b/spec/rubocop/cop/migration/create_table_with_foreign_keys_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/create_table_with_foreign_keys'
RSpec.describe RuboCop::Cop::Migration::CreateTableWithForeignKeys do
@@ -192,7 +192,7 @@ RSpec.describe RuboCop::Cop::Migration::CreateTableWithForeignKeys do
include_context 'when there is a target to a high traffic table', :foreign_key do
let(:explicit_target_opts) { ", to_table: :#{table_name}" }
- let(:implicit_target_opts) { }
+ let(:implicit_target_opts) {}
end
end
end
diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb
index 95a875b3baa..400abe3be70 100644
--- a/spec/rubocop/cop/migration/datetime_spec.rb
+++ b/spec/rubocop/cop/migration/datetime_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/datetime'
RSpec.describe RuboCop::Cop::Migration::Datetime do
- subject(:cop) { described_class.new }
-
let(:create_table_migration_without_datetime) do
%q(
class Users < ActiveRecord::Migration[6.0]
diff --git a/spec/rubocop/cop/migration/drop_table_spec.rb b/spec/rubocop/cop/migration/drop_table_spec.rb
index f1bd710f5e6..dd5f93eaafc 100644
--- a/spec/rubocop/cop/migration/drop_table_spec.rb
+++ b/spec/rubocop/cop/migration/drop_table_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/drop_table'
RSpec.describe RuboCop::Cop::Migration::DropTable do
- subject(:cop) { described_class.new }
-
context 'when in deployment migration' do
let(:msg) do
'`drop_table` in deployment migrations requires downtime. Drop tables in post-deployment migrations instead.'
diff --git a/spec/rubocop/cop/migration/migration_record_spec.rb b/spec/rubocop/cop/migration/migration_record_spec.rb
index bfe6228c421..96a1d8fa107 100644
--- a/spec/rubocop/cop/migration/migration_record_spec.rb
+++ b/spec/rubocop/cop/migration/migration_record_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/migration_record'
RSpec.describe RuboCop::Cop::Migration::MigrationRecord do
- subject(:cop) { described_class.new }
-
shared_examples 'a disabled cop' do |klass|
it 'does not register any offenses' do
expect_no_offenses(<<~SOURCE)
diff --git a/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb b/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb
index aa63259288d..1035ed2fb4a 100644
--- a/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction'
RSpec.describe RuboCop::Cop::Migration::PreventGlobalEnableLockRetriesWithDisableDdlTransaction do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/prevent_index_creation_spec.rb b/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
index ed7c8974d8d..9d886467a48 100644
--- a/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/prevent_index_creation'
RSpec.describe RuboCop::Cop::Migration::PreventIndexCreation do
- subject(:cop) { described_class.new }
-
let(:forbidden_tables) { %w(ci_builds) }
let(:forbidden_tables_list) { forbidden_tables.join(', ') }
diff --git a/spec/rubocop/cop/migration/prevent_strings_spec.rb b/spec/rubocop/cop/migration/prevent_strings_spec.rb
index d1760c2db88..f1adeae6786 100644
--- a/spec/rubocop/cop/migration/prevent_strings_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_strings_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/prevent_strings'
RSpec.describe RuboCop::Cop::Migration::PreventStrings do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb b/spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb
index c65f86d1e13..acdc6843584 100644
--- a/spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb
+++ b/spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
#
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/refer_to_index_by_name'
RSpec.describe RuboCop::Cop::Migration::ReferToIndexByName do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb
index f72a5b048d5..4aa842969fe 100644
--- a/spec/rubocop/cop/migration/remove_column_spec.rb
+++ b/spec/rubocop/cop/migration/remove_column_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/remove_column'
RSpec.describe RuboCop::Cop::Migration::RemoveColumn do
- subject(:cop) { described_class.new }
-
def source(meth = 'change')
"def #{meth}; remove_column :table, :column; end"
end
diff --git a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
index 10ca0353b0f..1d59390d659 100644
--- a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
+++ b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/remove_concurrent_index'
RSpec.describe RuboCop::Cop::Migration::RemoveConcurrentIndex do
- subject(:cop) { described_class.new }
-
context 'in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/remove_index_spec.rb b/spec/rubocop/cop/migration/remove_index_spec.rb
index 5d1ffef2589..24823b47d53 100644
--- a/spec/rubocop/cop/migration/remove_index_spec.rb
+++ b/spec/rubocop/cop/migration/remove_index_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/remove_index'
RSpec.describe RuboCop::Cop::Migration::RemoveIndex do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
index cf9bdbeef91..2050051cac7 100644
--- a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
+++ b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/safer_boolean_column'
RSpec.describe RuboCop::Cop::Migration::SaferBooleanColumn do
- subject(:cop) { described_class.new }
-
context 'in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/schedule_async_spec.rb b/spec/rubocop/cop/migration/schedule_async_spec.rb
index 09d2c77369c..59e03db07c0 100644
--- a/spec/rubocop/cop/migration/schedule_async_spec.rb
+++ b/spec/rubocop/cop/migration/schedule_async_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/schedule_async'
diff --git a/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb b/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
index 499351b3585..46c460b5d49 100644
--- a/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
+++ b/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/sidekiq_queue_migrate'
RSpec.describe RuboCop::Cop::Migration::SidekiqQueueMigrate do
- subject(:cop) { described_class.new }
-
def source(meth = 'change')
"def #{meth}; sidekiq_queue_migrate 'queue', to: 'new_queue'; end"
end
diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb
index 2f99a3ff35b..706fd8a3d0f 100644
--- a/spec/rubocop/cop/migration/timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/timestamps_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/timestamps'
RSpec.describe RuboCop::Cop::Migration::Timestamps do
- subject(:cop) { described_class.new }
-
let(:migration_with_timestamps) do
%q(
class Users < ActiveRecord::Migration[4.2]
diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
index a12ae94c22b..005d3fb6b2a 100644
--- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
+++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/update_column_in_batches'
diff --git a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
index d9b0cd4546c..b44f5d64a62 100644
--- a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
+++ b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/versioned_migration_class'
RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass do
- subject(:cop) { described_class.new }
-
let(:migration) do
<<~SOURCE
class TestMigration < Gitlab::Database::Migration[1.0]
diff --git a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
index 298ca273256..5762f78820c 100644
--- a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
+++ b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/with_lock_retries_disallowed_method'
RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb b/spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb
index f2e84a8697c..fed9176ea97 100644
--- a/spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb
+++ b/spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/with_lock_retries_with_change'
RSpec.describe RuboCop::Cop::Migration::WithLockRetriesWithChange do
- subject(:cop) { described_class.new }
-
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb b/spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb
index df18121e2df..ac58ca1edf3 100644
--- a/spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb
+++ b/spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rspec-parameterized'
require_relative '../../../../rubocop/cop/performance/active_record_subtransaction_methods'
RSpec.describe RuboCop::Cop::Performance::ActiveRecordSubtransactionMethods do
- subject(:cop) { described_class.new }
-
let(:message) { described_class::MSG }
shared_examples 'a method that uses a subtransaction' do |method_name|
diff --git a/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
index 0da2e30062a..e839a3e9367 100644
--- a/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
+++ b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/performance/active_record_subtransactions'
RSpec.describe RuboCop::Cop::Performance::ActiveRecordSubtransactions do
- subject(:cop) { described_class.new }
-
let(:message) { described_class::MSG }
context 'when calling #transaction with only requires_new: true' do
diff --git a/spec/rubocop/cop/performance/ar_count_each_spec.rb b/spec/rubocop/cop/performance/ar_count_each_spec.rb
index 4aeb9e13b18..a86b3f2b983 100644
--- a/spec/rubocop/cop/performance/ar_count_each_spec.rb
+++ b/spec/rubocop/cop/performance/ar_count_each_spec.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/performance/ar_count_each'
RSpec.describe RuboCop::Cop::Performance::ARCountEach do
- subject(:cop) { described_class.new }
-
context 'when it is not haml file' do
it 'does not flag it as an offense' do
- expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
+ expect(cop).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
expect_no_offenses <<~SOURCE
show(@users.count)
@@ -19,7 +17,7 @@ RSpec.describe RuboCop::Cop::Performance::ARCountEach do
context 'when it is haml file' do
before do
- expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
+ expect(cop).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
end
context 'when the same object uses count and each' do
diff --git a/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
index e95220756ed..070e792eeec 100644
--- a/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
+++ b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/performance/ar_exists_and_present_blank'
RSpec.describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
- subject(:cop) { described_class.new }
-
context 'when it is not haml file' do
it 'does not flag it as an offense' do
- expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
+ expect(cop).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
expect_no_offenses <<~SOURCE
return unless @users.exists?
@@ -19,7 +17,7 @@ RSpec.describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
context 'when it is haml file' do
before do
- expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
+ expect(cop).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
end
context 'the same object uses exists? and present?' do
diff --git a/spec/rubocop/cop/performance/readlines_each_spec.rb b/spec/rubocop/cop/performance/readlines_each_spec.rb
index 0a8b168ce5d..d876cbf79a5 100644
--- a/spec/rubocop/cop/performance/readlines_each_spec.rb
+++ b/spec/rubocop/cop/performance/readlines_each_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/performance/readlines_each'
RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
- subject(:cop) { described_class.new }
-
let(:message) { 'Avoid `IO.readlines.each`, since it reads contents into memory in full. Use `IO.each_line` or `IO.each` instead.' }
shared_examples_for(:class_read) do |klass|
diff --git a/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb b/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb
index 1261ca7891c..a2a4270c48e 100644
--- a/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb
+++ b/spec/rubocop/cop/prefer_class_methods_over_module_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/prefer_class_methods_over_module'
RSpec.describe RuboCop::Cop::PreferClassMethodsOverModule do
- subject(:cop) { described_class.new }
-
it 'flags violation when using module ClassMethods and corrects', :aggregate_failures do
expect_offense(<<~RUBY)
module Foo
diff --git a/spec/rubocop/cop/project_path_helper_spec.rb b/spec/rubocop/cop/project_path_helper_spec.rb
index b3c920f9d25..3153c928c77 100644
--- a/spec/rubocop/cop/project_path_helper_spec.rb
+++ b/spec/rubocop/cop/project_path_helper_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/project_path_helper'
RSpec.describe RuboCop::Cop::ProjectPathHelper do
- subject(:cop) { described_class.new }
-
context "when using namespace_project with the project's namespace" do
let(:source) { 'edit_namespace_project_issue_path(@issue.project.namespace, @issue.project, @issue)' }
let(:correct_source) { 'edit_project_issue_path(@issue.project, @issue)' }
diff --git a/spec/rubocop/cop/put_group_routes_under_scope_spec.rb b/spec/rubocop/cop/put_group_routes_under_scope_spec.rb
index 366fc4b5657..8697345cddc 100644
--- a/spec/rubocop/cop/put_group_routes_under_scope_spec.rb
+++ b/spec/rubocop/cop/put_group_routes_under_scope_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/put_group_routes_under_scope'
RSpec.describe RuboCop::Cop::PutGroupRoutesUnderScope do
- subject(:cop) { described_class.new }
-
%w[resource resources get post put patch delete].each do |route_method|
it "registers an offense when route is outside scope for `#{route_method}`" do
offense = "#{route_method} :notes"
diff --git a/spec/rubocop/cop/put_project_routes_under_scope_spec.rb b/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
index 9d226db09ef..65d330b0f05 100644
--- a/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
+++ b/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/put_project_routes_under_scope'
RSpec.describe RuboCop::Cop::PutProjectRoutesUnderScope do
- subject(:cop) { described_class.new }
-
%w[resource resources get post put patch delete].each do |route_method|
it "registers an offense when route is outside scope for `#{route_method}`" do
offense = "#{route_method} :notes"
diff --git a/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb b/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb
index 9335b8d01ee..ab270090c7d 100644
--- a/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb
+++ b/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/qa/ambiguous_page_object_name'
RSpec.describe RuboCop::Cop::QA::AmbiguousPageObjectName do
let(:source_file) { 'qa/page.rb' }
- subject(:cop) { described_class.new }
-
context 'in a QA file' do
before do
allow(cop).to receive(:in_qa_file?).and_return(true)
diff --git a/spec/rubocop/cop/qa/element_with_pattern_spec.rb b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
index d3e79525c62..1febdaf9c3b 100644
--- a/spec/rubocop/cop/qa/element_with_pattern_spec.rb
+++ b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/qa/element_with_pattern'
RSpec.describe RuboCop::Cop::QA::ElementWithPattern do
let(:source_file) { 'qa/page.rb' }
- subject(:cop) { described_class.new }
-
context 'in a QA file' do
before do
allow(cop).to receive(:in_qa_file?).and_return(true)
diff --git a/spec/rubocop/cop/qa/selector_usage_spec.rb b/spec/rubocop/cop/qa/selector_usage_spec.rb
index b40c57f8991..0ec289c1da6 100644
--- a/spec/rubocop/cop/qa/selector_usage_spec.rb
+++ b/spec/rubocop/cop/qa/selector_usage_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/qa/selector_usage'
RSpec.describe RuboCop::Cop::QA::SelectorUsage do
- subject(:cop) { described_class.new }
-
shared_examples 'non-qa file usage' do
it 'reports an offense' do
expect_offense(<<-RUBY)
diff --git a/spec/rubocop/cop/rspec/any_instance_of_spec.rb b/spec/rubocop/cop/rspec/any_instance_of_spec.rb
index e7675ded25e..f9675e17842 100644
--- a/spec/rubocop/cop/rspec/any_instance_of_spec.rb
+++ b/spec/rubocop/cop/rspec/any_instance_of_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/any_instance_of'
RSpec.describe RuboCop::Cop::RSpec::AnyInstanceOf do
- subject(:cop) { described_class.new }
-
context 'when calling allow_any_instance_of' do
let(:source) do
<<~SRC
diff --git a/spec/rubocop/cop/rspec/be_success_matcher_spec.rb b/spec/rubocop/cop/rspec/be_success_matcher_spec.rb
index 678e62048b8..c26fa32db8e 100644
--- a/spec/rubocop/cop/rspec/be_success_matcher_spec.rb
+++ b/spec/rubocop/cop/rspec/be_success_matcher_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/be_success_matcher'
RSpec.describe RuboCop::Cop::RSpec::BeSuccessMatcher do
let(:source_file) { 'spec/foo_spec.rb' }
- subject(:cop) { described_class.new }
-
shared_examples 'cop' do |good:, bad:|
context "using #{bad} call" do
it 'registers an offense and corrects', :aggregate_failures do
diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb
index 0fd09eeae11..6212cda0b88 100644
--- a/spec/rubocop/cop/rspec/env_assignment_spec.rb
+++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/env_assignment'
@@ -10,8 +10,6 @@ RSpec.describe RuboCop::Cop::RSpec::EnvAssignment do
let(:source_file) { 'spec/foo_spec.rb' }
- subject(:cop) { described_class.new }
-
shared_examples 'an offensive and correction ENV#[]= call' do |content, autocorrected_content|
it "registers an offense for `#{content}` and corrects", :aggregate_failures do
expect_offense(<<~CODE)
diff --git a/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb b/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb
index e36feecdd66..a07cf472ef0 100644
--- a/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb
+++ b/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/expect_gitlab_tracking'
RSpec.describe RuboCop::Cop::RSpec::ExpectGitlabTracking do
let(:source_file) { 'spec/foo_spec.rb' }
- subject(:cop) { described_class.new }
-
good_samples = [
'expect_snowplow_event(category: nil, action: nil)',
'expect_snowplow_event(category: "EventCategory", action: "event_action")',
diff --git a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
index 74c1521fa0e..e41dd338387 100644
--- a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
+++ b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/factories_in_migration_specs'
RSpec.describe RuboCop::Cop::RSpec::FactoriesInMigrationSpecs do
- subject(:cop) { described_class.new }
-
shared_examples 'an offensive factory call' do |namespace|
%i[build build_list create create_list attributes_for].each do |forbidden_method|
namespaced_forbidden_method = "#{namespace}#{forbidden_method}(:user)"
diff --git a/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb b/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb
index 194e2436ff2..008af734a99 100644
--- a/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb
+++ b/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rspec-parameterized'
require_relative '../../../../../rubocop/cop/rspec/factory_bot/inline_association'
RSpec.describe RuboCop::Cop::RSpec::FactoryBot::InlineAssociation do
- subject(:cop) { described_class.new }
-
shared_examples 'offense' do |code_snippet, autocorrected|
# We allow `create` or `FactoryBot.create` or `::FactoryBot.create`
let(:type) { code_snippet[/^(?:::)?(?:FactoryBot\.)?(\w+)/, 1] }
diff --git a/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb b/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb
index 9bdbe145f4c..e8a60b9cad7 100644
--- a/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb
+++ b/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rspec-parameterized'
require_relative '../../../../rubocop/cop/rspec/have_gitlab_http_status'
@@ -10,8 +10,6 @@ RSpec.describe RuboCop::Cop::RSpec::HaveGitlabHttpStatus do
let(:source_file) { 'spec/foo_spec.rb' }
- subject(:cop) { described_class.new }
-
shared_examples 'offense' do |bad, good|
it 'registers an offense', :aggregate_failures do
expect_offense(<<~CODE, node: bad)
diff --git a/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
index eac6ceb3ddf..537a7a9a7e9 100644
--- a/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
+++ b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/httparty_basic_auth'
RSpec.describe RuboCop::Cop::RSpec::HTTPartyBasicAuth do
- subject(:cop) { described_class.new }
-
context 'when passing `basic_auth: { user: ... }`' do
it 'registers an offense and corrects', :aggregate_failures do
expect_offense(<<~SOURCE, 'spec/foo.rb')
diff --git a/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb b/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb
index 7a2b7c92bd1..3227b075758 100644
--- a/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb
+++ b/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/modify_sidekiq_middleware'
RSpec.describe RuboCop::Cop::RSpec::ModifySidekiqMiddleware do
- subject(:cop) { described_class.new }
-
it 'registers an offense and corrects', :aggregate_failures do
expect_offense(<<~CODE)
Sidekiq::Testing.server_middleware do |chain|
diff --git a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
index b8d16d58d9e..4361f587da3 100644
--- a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
+++ b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/timecop_freeze'
RSpec.describe RuboCop::Cop::RSpec::TimecopFreeze do
- subject(:cop) { described_class.new }
-
context 'when calling Timecop.freeze' do
it 'registers an offense and corrects', :aggregate_failures do
expect_offense(<<~CODE)
diff --git a/spec/rubocop/cop/rspec/timecop_travel_spec.rb b/spec/rubocop/cop/rspec/timecop_travel_spec.rb
index 16e09fb8c45..89c46ff6c59 100644
--- a/spec/rubocop/cop/rspec/timecop_travel_spec.rb
+++ b/spec/rubocop/cop/rspec/timecop_travel_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/timecop_travel'
RSpec.describe RuboCop::Cop::RSpec::TimecopTravel do
- subject(:cop) { described_class.new }
-
context 'when calling Timecop.travel' do
it 'registers an offense and corrects', :aggregate_failures do
expect_offense(<<~CODE)
diff --git a/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb b/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb
index 78e6bec51d4..90101e09023 100644
--- a/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb
+++ b/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/top_level_describe_path'
RSpec.describe RuboCop::Cop::RSpec::TopLevelDescribePath do
- subject(:cop) { described_class.new }
-
context 'when the file ends in _spec.rb' do
it 'registers no offenses' do
expect_no_offenses(<<~SOURCE, 'spec/foo_spec.rb')
diff --git a/spec/rubocop/cop/rspec/web_mock_enable_spec.rb b/spec/rubocop/cop/rspec/web_mock_enable_spec.rb
index 61a85064a61..63ffc06f1ca 100644
--- a/spec/rubocop/cop/rspec/web_mock_enable_spec.rb
+++ b/spec/rubocop/cop/rspec/web_mock_enable_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rspec/web_mock_enable'
RSpec.describe RuboCop::Cop::RSpec::WebMockEnable do
- subject(:cop) { described_class.new }
-
context 'when calling WebMock.disable_net_connect!' do
it 'registers an offence and autocorrects it' do
expect_offense(<<~RUBY)
diff --git a/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb b/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb
index c21999be917..b687e91601c 100644
--- a/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb
+++ b/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/ruby_interpolation_in_translation'
@@ -9,8 +9,6 @@ require_relative '../../../rubocop/cop/ruby_interpolation_in_translation'
RSpec.describe RuboCop::Cop::RubyInterpolationInTranslation do
let(:msg) { "Don't use ruby interpolation \#{} inside translated strings, instead use %{}" }
- subject(:cop) { described_class.new }
-
it 'does not add an offense for a regular messages' do
expect_no_offenses('_("Hello world")')
end
diff --git a/spec/rubocop/cop/safe_params_spec.rb b/spec/rubocop/cop/safe_params_spec.rb
index 9a064b93b16..e6d86019d18 100644
--- a/spec/rubocop/cop/safe_params_spec.rb
+++ b/spec/rubocop/cop/safe_params_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/safe_params'
RSpec.describe RuboCop::Cop::SafeParams do
- subject(:cop) { described_class.new }
-
it 'flags the params as an argument of url_for' do
expect_offense(<<~SOURCE)
url_for(params)
diff --git a/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb b/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
index 74912b53d37..bd248cd028a 100644
--- a/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
+++ b/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/scalability/bulk_perform_with_context'
RSpec.describe RuboCop::Cop::Scalability::BulkPerformWithContext do
- subject(:cop) { described_class.new }
-
it "adds an offense when calling bulk_perform_async" do
expect_offense(<<~CODE)
Worker.bulk_perform_async(args)
diff --git a/spec/rubocop/cop/scalability/cron_worker_context_spec.rb b/spec/rubocop/cop/scalability/cron_worker_context_spec.rb
index 28db12fd075..bcf93b04d6a 100644
--- a/spec/rubocop/cop/scalability/cron_worker_context_spec.rb
+++ b/spec/rubocop/cop/scalability/cron_worker_context_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/scalability/cron_worker_context'
RSpec.describe RuboCop::Cop::Scalability::CronWorkerContext do
- subject(:cop) { described_class.new }
-
it 'adds an offense when including CronjobQueue' do
expect_offense(<<~CODE)
class SomeWorker
diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb
index ca25b0246f0..1395615479f 100644
--- a/spec/rubocop/cop/scalability/file_uploads_spec.rb
+++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/scalability/file_uploads'
RSpec.describe RuboCop::Cop::Scalability::FileUploads do
- subject(:cop) { described_class.new }
-
let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' }
context 'with required params' do
diff --git a/spec/rubocop/cop/scalability/idempotent_worker_spec.rb b/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
index 53c0c06f6c9..b1984721803 100644
--- a/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
+++ b/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/scalability/idempotent_worker'
RSpec.describe RuboCop::Cop::Scalability::IdempotentWorker do
- subject(:cop) { described_class.new }
-
before do
allow(cop)
.to receive(:in_worker?)
diff --git a/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_spec.rb b/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_spec.rb
index cf8d0d1b66f..7b6578a0744 100644
--- a/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_spec.rb
+++ b/spec/rubocop/cop/sidekiq_load_balancing/worker_data_consistency_spec.rb
@@ -1,11 +1,9 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/sidekiq_load_balancing/worker_data_consistency'
RSpec.describe RuboCop::Cop::SidekiqLoadBalancing::WorkerDataConsistency do
- subject(:cop) { described_class.new }
-
before do
allow(cop)
.to receive(:in_worker?)
diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
index 346a8d82475..da126090a81 100644
--- a/spec/rubocop/cop/sidekiq_options_queue_spec.rb
+++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/sidekiq_options_queue'
RSpec.describe RuboCop::Cop::SidekiqOptionsQueue do
- subject(:cop) { described_class.new }
-
it 'registers an offense when `sidekiq_options` is used with the `queue` option' do
expect_offense(<<~CODE)
sidekiq_options queue: "some_queue"
diff --git a/spec/rubocop/cop/static_translation_definition_spec.rb b/spec/rubocop/cop/static_translation_definition_spec.rb
index 372fc194c56..10b4f162504 100644
--- a/spec/rubocop/cop/static_translation_definition_spec.rb
+++ b/spec/rubocop/cop/static_translation_definition_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rspec-parameterized'
@@ -11,8 +11,6 @@ RSpec.describe RuboCop::Cop::StaticTranslationDefinition do
let(:msg) { described_class::MSG }
- subject(:cop) { described_class.new }
-
shared_examples 'offense' do |code|
it 'registers an offense' do
expect_offense(code)
diff --git a/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb b/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb
index 384a834a512..1d1c0852db2 100644
--- a/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb
+++ b/spec/rubocop/cop/style/regexp_literal_mixed_preserve_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/style/regexp_literal_mixed_preserve'
diff --git a/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb b/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
index f377dfe36d8..b4d113a9bcc 100644
--- a/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
+++ b/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/usage_data/distinct_count_by_large_foreign_key'
@@ -13,8 +13,6 @@ RSpec.describe RuboCop::Cop::UsageData::DistinctCountByLargeForeignKey do
})
end
- subject(:cop) { described_class.new(config) }
-
context 'when counting by disallowed key' do
it 'registers an offense' do
expect_offense(<<~CODE)
diff --git a/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb b/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
index 56aecc3ec4e..efa4e27dc9c 100644
--- a/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
+++ b/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/usage_data/histogram_with_large_table'
@@ -14,8 +14,6 @@ RSpec.describe RuboCop::Cop::UsageData::HistogramWithLargeTable do
})
end
- subject(:cop) { described_class.new(config) }
-
context 'with large tables' do
context 'with one-level constants' do
context 'when calling histogram(Issue)' do
diff --git a/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb b/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
index 31324331e61..a55f0852f35 100644
--- a/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
+++ b/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/usage_data/instrumentation_superclass'
@@ -14,8 +14,6 @@ RSpec.describe RuboCop::Cop::UsageData::InstrumentationSuperclass do
})
end
- subject(:cop) { described_class.new(config) }
-
context 'with class definition' do
context 'when inheriting from allowed superclass' do
it 'does not register an offense' do
diff --git a/spec/rubocop/cop/usage_data/large_table_spec.rb b/spec/rubocop/cop/usage_data/large_table_spec.rb
index a6b22fd7f0d..fa94f878cea 100644
--- a/spec/rubocop/cop/usage_data/large_table_spec.rb
+++ b/spec/rubocop/cop/usage_data/large_table_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/usage_data/large_table'
@@ -18,8 +18,6 @@ RSpec.describe RuboCop::Cop::UsageData::LargeTable do
})
end
- subject(:cop) { described_class.new(config) }
-
context 'when in usage_data files' do
before do
allow(cop).to receive(:usage_data_files?).and_return(true)
diff --git a/spec/rubocop/cop/user_admin_spec.rb b/spec/rubocop/cop/user_admin_spec.rb
index 3bf458348f3..99e87d619c0 100644
--- a/spec/rubocop/cop/user_admin_spec.rb
+++ b/spec/rubocop/cop/user_admin_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rubocop'
require_relative '../../../rubocop/cop/user_admin'
RSpec.describe RuboCop::Cop::UserAdmin do
- subject(:cop) { described_class.new }
-
it 'flags a method call' do
expect_offense(<<~SOURCE)
user.admin?
diff --git a/spec/rubocop/cop_todo_spec.rb b/spec/rubocop/cop_todo_spec.rb
index 978df2c01ee..3f9c378b303 100644
--- a/spec/rubocop/cop_todo_spec.rb
+++ b/spec/rubocop/cop_todo_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require_relative '../../rubocop/cop_todo'
RSpec.describe RuboCop::CopTodo do
@@ -14,7 +14,8 @@ RSpec.describe RuboCop::CopTodo do
cop_name: cop_name,
files: be_empty,
offense_count: 0,
- previously_disabled: false
+ previously_disabled: false,
+ grace_period: false
)
end
end
@@ -102,6 +103,23 @@ RSpec.describe RuboCop::CopTodo do
end
end
+ context 'with grace period' do
+ specify do
+ cop_todo.record('a.rb', 1)
+ cop_todo.record('b.rb', 2)
+ cop_todo.grace_period = true
+
+ expect(yaml).to eq(<<~YAML)
+ ---
+ #{cop_name}:
+ Details: grace period
+ Exclude:
+ - 'a.rb'
+ - 'b.rb'
+ YAML
+ end
+ end
+
context 'with multiple files' do
before do
cop_todo.record('a.rb', 0)
diff --git a/spec/rubocop/formatter/graceful_formatter_spec.rb b/spec/rubocop/formatter/graceful_formatter_spec.rb
new file mode 100644
index 00000000000..0e0c1d52067
--- /dev/null
+++ b/spec/rubocop/formatter/graceful_formatter_spec.rb
@@ -0,0 +1,239 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require 'rubocop/rspec/shared_contexts'
+require 'stringio'
+
+require_relative '../../../rubocop/formatter/graceful_formatter'
+require_relative '../../../rubocop/todo_dir'
+
+RSpec.describe RuboCop::Formatter::GracefulFormatter, :isolated_environment do
+ # Set by :isolated_environment
+ let(:todo_dir) { RuboCop::TodoDir.new("#{Dir.pwd}/.rubocop_todo") }
+ let(:stdout) { StringIO.new }
+
+ subject(:formatter) { described_class.new(stdout) }
+
+ shared_examples 'summary reporting' do |inspected:, offenses: 0, silenced: 0|
+ it "reports summary with #{inspected} inspected, #{offenses} offenses, #{silenced} silenced" do
+ expect(stdout.string)
+ .to match(/Inspecting #{inspected} files/)
+ .and match(/#{inspected} files inspected/)
+
+ if offenses > 0
+ expect(stdout.string).to match(/Offenses:/)
+ expect(stdout.string).to match(/#{offenses} offenses detected/)
+ else
+ expect(stdout.string).not_to match(/Offenses:/)
+ expect(stdout.string).to match(/no offenses detected/)
+ end
+
+ if silenced > 0
+ expect(stdout.string).to match(/Silenced offenses:/)
+ expect(stdout.string).to match(/#{silenced} offenses silenced/)
+ else
+ expect(stdout.string).not_to match(/Silenced offenses:/)
+ expect(stdout.string).not_to match(/offenses silenced/)
+ end
+ end
+ end
+
+ context 'with offenses' do
+ let(:offense1) { fake_offense('Cop1') }
+ let(:offense2) { fake_offense('Cop2') }
+
+ before do
+ FileUtils.touch('.rubocop_todo.yml')
+
+ File.write('.rubocop.yml', <<~YAML)
+ inherit_from:
+ <% Dir.glob('.rubocop_todo/**/*.yml').each do |rubocop_todo_yaml| %>
+ - '<%= rubocop_todo_yaml %>'
+ <% end %>
+ - '.rubocop_todo.yml'
+
+ AllCops:
+ NewCops: enable # Avoiding RuboCop warnings
+ YAML
+
+ # These cops are unknown and would raise an validation error
+ allow(RuboCop::Cop::Registry.global).to receive(:contains_cop_matching?)
+ .and_return(true)
+ end
+
+ context 'with active only' do
+ before do
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [offense1])
+ formatter.file_finished('b.rb', [offense2])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2, offenses: 2
+ end
+
+ context 'with silenced only' do
+ before do
+ todo_dir.write('Cop1', <<~YAML)
+ ---
+ Cop1:
+ Details: grace period
+ YAML
+
+ File.write('.rubocop_todo.yml', <<~YAML)
+ ---
+ Cop2:
+ Details: grace period
+ YAML
+
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [offense1])
+ formatter.file_finished('b.rb', [offense2])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2, silenced: 2
+ end
+
+ context 'with active and silenced' do
+ before do
+ todo_dir.write('Cop1', <<~YAML)
+ ---
+ Cop1:
+ Details: grace period
+ YAML
+
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [offense1, offense2])
+ formatter.file_finished('b.rb', [offense2, offense1, offense1])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2, offenses: 2, silenced: 3
+ end
+ end
+
+ context 'without offenses' do
+ before do
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [])
+ formatter.file_finished('b.rb', [])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2
+ end
+
+ context 'without files to inspect' do
+ before do
+ formatter.started([])
+ formatter.finished([])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 0
+ end
+
+ context 'with missing @total_offense_count' do
+ it 'raises an error' do
+ formatter.started(%w[a.rb])
+
+ if formatter.instance_variable_defined?(:@total_offense_count)
+ formatter.remove_instance_variable(:@total_offense_count)
+ end
+
+ expect do
+ formatter.finished(%w[a.rb])
+ end.to raise_error(/RuboCop has changed its internals/)
+ end
+ end
+
+ describe '.adjusted_exit_status' do
+ using RSpec::Parameterized::TableSyntax
+
+ success = RuboCop::CLI::STATUS_SUCCESS
+ offenses = RuboCop::CLI::STATUS_OFFENSES
+ error = RuboCop::CLI::STATUS_ERROR
+
+ subject { described_class.adjusted_exit_status(status) }
+
+ where(:active_offenses, :status, :adjusted_status) do
+ 0 | success | success
+ 0 | offenses | success
+ 1 | offenses | offenses
+ 0 | error | error
+ 1 | error | error
+ # impossible cases
+ 1 | success | success
+ end
+
+ with_them do
+ around do |example|
+ described_class.active_offenses = active_offenses
+ example.run
+ ensure
+ described_class.active_offenses = 0
+ end
+
+ it { is_expected.to eq(adjusted_status) }
+ end
+ end
+
+ describe '.grace_period?' do
+ let(:cop_name) { 'Cop/Name' }
+
+ subject { described_class.grace_period?(cop_name, config) }
+
+ context 'with Details in config' do
+ let(:config) { { 'Details' => 'grace period' } }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with unknown value for Details in config' do
+ let(:config) { { 'Details' => 'unknown' } }
+
+ specify do
+ expect { is_expected.to eq(false) }
+ .to output(/#{cop_name}: Unhandled value "unknown" for `Details` key./)
+ .to_stderr
+ end
+ end
+
+ context 'with empty config' do
+ let(:config) { {} }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'without Details in config' do
+ let(:config) { { 'Exclude' => false } }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.grace_period_key_value' do
+ subject { described_class.grace_period_key_value }
+
+ it { is_expected.to eq('Details: grace period') }
+ end
+
+ def fake_offense(cop_name)
+ # rubocop:disable RSpec/VerifiedDoubles
+ double(:offense,
+ cop_name: cop_name,
+ corrected?: false,
+ correctable?: false,
+ severity: double(:severity, name: 'convention', code: :C),
+ line: 5,
+ column: 23,
+ real_column: 23,
+ corrected_with_todo?: false,
+ message: "#{cop_name} message",
+ location: double(:location, source_line: 'line', first_line: 1, last_line: 2),
+ highlighted_area: double(:highlighted_area, begin_pos: 1, size: 2)
+ )
+ # rubocop:enable RSpec/VerifiedDoubles
+ end
+end
diff --git a/spec/rubocop/formatter/todo_formatter_spec.rb b/spec/rubocop/formatter/todo_formatter_spec.rb
index df56ee45931..edd84632409 100644
--- a/spec/rubocop/formatter/todo_formatter_spec.rb
+++ b/spec/rubocop/formatter/todo_formatter_spec.rb
@@ -2,8 +2,10 @@
# rubocop:disable RSpec/VerifiedDoubles
require 'fast_spec_helper'
-require 'stringio'
+
require 'fileutils'
+require 'stringio'
+require 'tmpdir'
require_relative '../../../rubocop/formatter/todo_formatter'
require_relative '../../../rubocop/todo_dir'
@@ -174,6 +176,98 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
end
end
+ context 'with grace period' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ B/TooManyOffenses:
+ Details: grace period
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ shared_examples 'keeps grace period' do
+ it 'keeps Details: grace period' do
+ run_formatter
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Details: grace period
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'in rubocop_todo/' do
+ before do
+ todo_dir.write('B/TooManyOffenses', yaml)
+ todo_dir.inspect_all
+ end
+
+ it_behaves_like 'keeps grace period'
+ end
+
+ context 'in rubocop_todo.yml' do
+ before do
+ File.write('.rubocop_todo.yml', yaml)
+ end
+
+ it_behaves_like 'keeps grace period'
+ end
+
+ context 'with invalid details value' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ B/TooManyOffenses:
+ Details: something unknown
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ it 'ignores the details and warns' do
+ File.write('.rubocop_todo.yml', yaml)
+
+ expect { run_formatter }
+ .to output(%r{B/TooManyOffenses: Unhandled value "something unknown" for `Details` key.})
+ .to_stderr
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'and previously disabled' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ B/TooManyOffenses:
+ Enabled: false
+ Details: grace period
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ it 'raises an exception' do
+ File.write('.rubocop_todo.yml', yaml)
+
+ expect { run_formatter }
+ .to raise_error(RuntimeError, 'B/TooManyOffenses: Cop must be enabled to use `Details: grace period`.')
+ end
+ end
+ end
+
context 'with cop configuration in both .rubocop_todo/ and .rubocop_todo.yml' do
before do
todo_dir.write('B/TooManyOffenses', <<~YAML)
diff --git a/spec/rubocop/qa_helpers_spec.rb b/spec/rubocop/qa_helpers_spec.rb
index 4b5566609e3..a50c8307733 100644
--- a/spec/rubocop/qa_helpers_spec.rb
+++ b/spec/rubocop/qa_helpers_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'parser/current'
require_relative '../../rubocop/qa_helpers'
diff --git a/spec/rubocop/todo_dir_spec.rb b/spec/rubocop/todo_dir_spec.rb
index a5c12e23896..e014c4c2c37 100644
--- a/spec/rubocop/todo_dir_spec.rb
+++ b/spec/rubocop/todo_dir_spec.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require 'fileutils'
+
require 'active_support/inflector/inflections'
+require 'fileutils'
+require 'tmpdir'
require_relative '../../rubocop/todo_dir'
diff --git a/spec/rubocop_spec_helper.rb b/spec/rubocop_spec_helper.rb
new file mode 100644
index 00000000000..a37415a25de
--- /dev/null
+++ b/spec/rubocop_spec_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# All RuboCop specs may use fast_spec_helper.
+require 'fast_spec_helper'
+
+# To prevent load order issues we need to require `rubocop` first.
+# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47008
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+RSpec.configure do |config|
+ config.include RuboCop::RSpec::ExpectOffense, type: :rubocop
+
+ config.define_derived_metadata(file_path: %r{spec/rubocop}) do |metadata|
+ metadata[:type] = :rubocop
+ end
+
+ # Include config shared context for all cop specs.
+ config.include_context 'config', type: :rubocop
+end
diff --git a/spec/scripts/changed-feature-flags_spec.rb b/spec/scripts/changed-feature-flags_spec.rb
index f4058614d85..f1e381b0656 100644
--- a/spec/scripts/changed-feature-flags_spec.rb
+++ b/spec/scripts/changed-feature-flags_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tmpdir'
load File.expand_path('../../scripts/changed-feature-flags', __dir__)
diff --git a/spec/scripts/determine-qa-tests_spec.rb b/spec/scripts/determine-qa-tests_spec.rb
deleted file mode 100644
index 043eb7f2dc9..00000000000
--- a/spec/scripts/determine-qa-tests_spec.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-# frozen_string_literal: true
-require 'fast_spec_helper'
-
-load File.expand_path('../../scripts/determine-qa-tests', __dir__)
-
-RSpec.describe 'scripts/determine-qa-tests' do
- describe DetermineQATests do
- describe '.execute' do
- let(:qa_spec_files) do
- %w[qa/qa/specs/features/browser_ui/1_manage/test1.rb
- qa/qa/specs/features/browser_ui/1_manage/user/test2.rb]
- end
-
- let(:qa_spec_and_non_spec_files) do
- %w[qa/qa/specs/features/browser_ui/1_manage/test1.rb
- qa/qa/page/admin/menu.rb]
- end
-
- let(:non_qa_files) do
- %w[rubocop/code_reuse_helpers.rb
- app/components/diffs/overflow_warning_component.rb]
- end
-
- let(:non_qa_and_feature_flag_files) do
- %w[rubocop/code_reuse_helpers.rb
- app/components/diffs/overflow_warning_component.rb
- config/feature_flags/development/access_token_ajax.yml]
- end
-
- let(:qa_spec_and_non_qa_files) do
- %w[rubocop/code_reuse_helpers.rb
- app/components/diffs/overflow_warning_component.rb
- qa/qa/specs/features/browser_ui/1_manage/test1.rb]
- end
-
- let(:qa_non_spec_and_non_qa_files) do
- %w[rubocop/code_reuse_helpers.rb
- app/components/diffs/overflow_warning_component.rb
- qa/qa/page/admin/menu.rb]
- end
-
- shared_examples 'determine qa tests' do
- context 'when only qa spec files have changed' do
- it 'returns only the changed qa specs' do
- subject = described_class.new({ changed_files: qa_spec_files }.merge(labels))
-
- expect(subject.execute).to eql qa_spec_files.map { |path| path.delete_prefix("qa/") }.join(' ')
- end
- end
-
- context 'when qa spec and non spec files have changed' do
- it 'does not return any specs' do
- subject = described_class.new({ changed_files: qa_spec_and_non_spec_files }.merge(labels))
- expect(subject.execute).to be_nil
- end
- end
-
- context 'when non-qa and feature flag files have changed' do
- it 'does not return any specs' do
- subject = described_class.new({ changed_files: non_qa_and_feature_flag_files }.merge(labels))
- expect(subject.execute).to be_nil
- end
- end
-
- context 'when qa spec and non-qa files have changed' do
- it 'does not return any specs' do
- subject = described_class.new({ changed_files: qa_spec_and_non_qa_files }.merge(labels))
- expect(subject.execute).to be_nil
- end
- end
-
- context 'when qa non-spec and non-qa files have changed' do
- it 'does not return any specs' do
- subject = described_class.new({ changed_files: qa_non_spec_and_non_qa_files }.merge(labels))
- expect(subject.execute).to be_nil
- end
- end
- end
-
- context 'when a devops label is not specified' do
- let(:labels) { { mr_labels: ['type::feature'] } }
-
- it_behaves_like 'determine qa tests'
-
- context 'when only non-qa files have changed' do
- it 'does not return any specs' do
- subject = described_class.new({ changed_files: non_qa_files })
- expect(subject.execute).to be_nil
- end
- end
- end
-
- context 'when a devops label is specified' do
- let(:labels) { { mr_labels: %w[devops::manage type::feature] } }
-
- it_behaves_like 'determine qa tests'
-
- context 'when only non-qa files have changed' do
- it 'returns the specs for the devops label' do
- subject = described_class.new({ changed_files: non_qa_files }.merge(labels))
- allow(subject).to receive(:qa_spec_directories_for_devops_stage)
- .and_return(['qa/qa/specs/features/browser_ui/1_manage/'])
- expect(subject.execute).to eql 'qa/specs/features/browser_ui/1_manage/'
- end
- end
- end
- end
- end
-end
diff --git a/spec/scripts/lib/glfm/parse_examples_spec.rb b/spec/scripts/lib/glfm/parse_examples_spec.rb
new file mode 100644
index 00000000000..a1ee6b3f440
--- /dev/null
+++ b/spec/scripts/lib/glfm/parse_examples_spec.rb
@@ -0,0 +1,331 @@
+# frozen_string_literal: true
+require 'fast_spec_helper'
+require_relative '../../../../scripts/lib/glfm/parse_examples'
+
+RSpec.describe Glfm::ParseExamples, '#parse_examples' do
+ subject do
+ described_module = described_class
+ Class.new { include described_module }.new
+ end
+
+ let(:spec_txt_contents) do
+ <<~MARKDOWN
+ ---
+ title: Spec
+ ...
+
+ # Introduction
+
+ intro
+
+ # 1.0.0 H1
+
+ ## 1.1.0 H2
+
+ no extension
+
+ ```````````````````````````````` example
+ example 1 md
+ .
+ html
+ ````````````````````````````````
+
+ one extension
+
+ ```````````````````````````````` example extension_1.1.0-1
+ example 2 md
+ .
+ html
+ ````````````````````````````````
+
+ ### 1.1.1 H3 with no examples
+
+ text
+
+ ### 1.1.2 Consecutive H3 with example
+
+ text
+
+ ```````````````````````````````` example disabled
+ example 3 md
+ .
+ html
+ ````````````````````````````````
+
+ ## 1.2.0 H2 with all disabled examples
+
+
+ ```````````````````````````````` example disabled
+ example 4 md
+ .
+ html
+ ````````````````````````````````
+
+ ## 1.2.0 New H2
+
+
+ ```````````````````````````````` example extension_1.2.0-1
+ example 5 md
+ .
+ html
+ ````````````````````````````````
+
+ # 2.0.0 New H1
+
+ ## 2.1.0 H2
+
+ ```````````````````````````````` example gitlab
+ example 6 md
+ .
+ html
+ ````````````````````````````````
+
+ ## 2.2.0 H2 which contains an H3
+
+ No example here, just text
+
+ ### 2.2.1 H3
+
+ The CommonMark and GHFM specifications don't have any examples inside an H3, but it is
+ supported for the GLFM specification.
+
+ ```````````````````````````````` example extension_2.2.1-1
+ example 7 md
+ .
+ html
+ ````````````````````````````````
+
+ # 3.0.0 New H1
+
+ ## 3.1.0 H2
+
+ ```````````````````````````````` example
+ example 8 md
+ .
+ html
+ ````````````````````````````````
+
+ ### 3.1.1 H3
+
+ ```````````````````````````````` example
+ example 9 md
+ .
+ html
+ ````````````````````````````````
+
+ ### 3.1.1 Consecutive H3
+
+ ```````````````````````````````` example
+ example 10 md
+ .
+ html
+ ````````````````````````````````
+
+ ## 3.2.0 Another H2
+
+ ```````````````````````````````` example
+ example 11 md
+ .
+ html
+ ````````````````````````````````
+
+ <!-- END TESTS -->
+
+ # Appendix
+
+ Appendix text.
+ MARKDOWN
+ end
+
+ let(:spec_txt_lines) { spec_txt_contents.split("\n") }
+
+ describe "parsing" do
+ it 'correctly parses' do
+ examples = subject.parse_examples(spec_txt_lines)
+
+ expected =
+ [
+ {
+ disabled: false,
+ end_line: 19,
+ example: 1,
+ extensions: [],
+ headers: [
+ '1.0.0 H1',
+ '1.1.0 H2'
+ ],
+ html: 'html',
+ markdown: 'example 1 md',
+ section: '1.1.0 H2',
+ start_line: 15
+ },
+ {
+ disabled: false,
+ end_line: 27,
+ example: 2,
+ extensions: %w[extension_1.1.0-1],
+ headers: [
+ '1.0.0 H1',
+ '1.1.0 H2'
+ ],
+ html: 'html',
+ markdown: 'example 2 md',
+ section: '1.1.0 H2',
+ start_line: 23
+ },
+ {
+ disabled: true,
+ end_line: 41,
+ example: 3,
+ extensions: %w[disabled],
+ headers: [
+ '1.0.0 H1',
+ '1.1.0 H2',
+ '1.1.2 Consecutive H3 with example'
+ ],
+ html: 'html',
+ markdown: 'example 3 md',
+ section: '1.1.2 Consecutive H3 with example',
+ start_line: 37
+ },
+ {
+ disabled: true,
+ end_line: 50,
+ example: 4,
+ extensions: %w[disabled],
+ headers: [
+ '1.0.0 H1',
+ '1.2.0 H2 with all disabled examples'
+ ],
+ html: 'html',
+ markdown: 'example 4 md',
+ section: '1.2.0 H2 with all disabled examples',
+ start_line: 46
+ },
+ {
+ disabled: false,
+ end_line: 59,
+ example: 5,
+ extensions: %w[extension_1.2.0-1],
+ headers: [
+ '1.0.0 H1',
+ '1.2.0 New H2'
+ ],
+ html: 'html',
+ markdown: 'example 5 md',
+ section: '1.2.0 New H2',
+ start_line: 55
+ },
+ {
+ disabled: false,
+ end_line: 69,
+ example: 6,
+ extensions: %w[gitlab],
+ headers: [
+ '2.0.0 New H1',
+ '2.1.0 H2'
+ ],
+ html: 'html',
+ markdown: 'example 6 md',
+ section: '2.1.0 H2',
+ start_line: 65
+ },
+ {
+ disabled: false,
+ end_line: 84,
+ example: 7,
+ extensions: %w[extension_2.2.1-1],
+ headers: [
+ '2.0.0 New H1',
+ '2.2.0 H2 which contains an H3',
+ '2.2.1 H3'
+ ],
+ html: 'html',
+ markdown: 'example 7 md',
+ section: '2.2.1 H3',
+ start_line: 80
+ },
+ {
+ disabled: false,
+ end_line: 94,
+ example: 8,
+ extensions: [],
+ headers: [
+ '3.0.0 New H1',
+ '3.1.0 H2'
+ ],
+ html: 'html',
+ markdown: 'example 8 md',
+ section: '3.1.0 H2',
+ start_line: 90
+ },
+ {
+ disabled: false,
+ end_line: 102,
+ example: 9,
+ extensions: [],
+ headers: [
+ '3.0.0 New H1',
+ '3.1.0 H2',
+ '3.1.1 H3'
+ ],
+ html: 'html',
+ markdown: 'example 9 md',
+ section: '3.1.1 H3',
+ start_line: 98
+ },
+ {
+ disabled: false,
+ end_line: 110,
+ example: 10,
+ extensions: [],
+ headers: [
+ '3.0.0 New H1',
+ '3.1.0 H2',
+ '3.1.1 Consecutive H3'
+ ],
+ html: 'html',
+ markdown: 'example 10 md',
+ section: '3.1.1 Consecutive H3',
+ start_line: 106
+ },
+ {
+ disabled: false,
+ end_line: 118,
+ example: 11,
+ extensions: [],
+ headers: [
+ '3.0.0 New H1',
+ '3.2.0 Another H2'
+ ],
+ html: 'html',
+ markdown: 'example 11 md',
+ section: '3.2.0 Another H2',
+ start_line: 114
+ }
+ ]
+
+ expect(examples).to eq(expected)
+ end
+ end
+
+ describe "with incorrect header nesting" do
+ let(:spec_txt_contents) do
+ <<~MARKDOWN
+ ---
+ title: Spec
+ ...
+
+ # H1
+
+ ### H3
+
+ MARKDOWN
+ end
+
+ it "raises if H3 is nested directly in H1" do
+ expect { subject.parse_examples(spec_txt_lines) }
+ .to raise_error(/The H3 'H3' may not be nested directly within the H1 'H1'/)
+ end
+ end
+end
diff --git a/spec/scripts/lib/glfm/shared_spec.rb b/spec/scripts/lib/glfm/shared_spec.rb
index f6792b93718..3ce9d44ba3d 100644
--- a/spec/scripts/lib/glfm/shared_spec.rb
+++ b/spec/scripts/lib/glfm/shared_spec.rb
@@ -9,6 +9,16 @@ RSpec.describe Glfm::Shared do
end.new
end
+ describe '#write_file' do
+ it 'works' do
+ filename = Dir::Tmpname.create('basename') do |path|
+ instance.write_file(path, 'test')
+ end
+
+ expect(File.read(filename)).to eq 'test'
+ end
+ end
+
describe '#run_external_cmd' do
it 'works' do
expect(instance.run_external_cmd('echo "hello"')).to eq("hello\n")
@@ -24,6 +34,14 @@ RSpec.describe Glfm::Shared do
end
end
+ describe '#dump_yaml_with_formatting' do
+ it 'works' do
+ hash = { a: 'b' }
+ yaml = instance.dump_yaml_with_formatting(hash, literal_scalars: true)
+ expect(yaml).to eq("---\na: |-\n b\n")
+ end
+ end
+
describe '#output' do
# NOTE: The #output method is normally always mocked, to prevent output while the specs are
# running. However, in order to provide code coverage for the method, we have to invoke
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index fe815aa6f1e..f96936c0a6f 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -35,6 +35,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
let(:glfm_spec_txt_local_io) { StringIO.new(glfm_spec_txt_contents) }
let(:glfm_example_status_yml_path) { described_class::GLFM_EXAMPLE_STATUS_YML_PATH }
let(:glfm_example_status_yml_io) { StringIO.new(glfm_example_status_yml_contents) }
+ let(:glfm_example_metadata_yml_path) { described_class::GLFM_EXAMPLE_METADATA_YML_PATH }
+ let(:glfm_example_metadata_yml_io) { StringIO.new(glfm_example_metadata_yml_contents) }
# Example Snapshot (ES) output files
let(:es_examples_index_yml_path) { described_class::ES_EXAMPLES_INDEX_YML_PATH }
@@ -52,7 +54,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
let(:static_html_tempfile_path) { Tempfile.new.path }
let(:glfm_spec_txt_contents) do
- <<~GLFM_SPEC_TXT_CONTENTS
+ <<~MARKDOWN
---
title: GitLab Flavored Markdown Spec
...
@@ -128,12 +130,25 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
## Strong but with two asterisks
- ```````````````````````````````` example gitlab strong
+ ```````````````````````````````` example gitlab
**bold**
.
<p><strong>bold</strong></p>
````````````````````````````````
+ ## H2 which contains an H3
+
+ ### Example in an H3
+
+ The CommonMark and GHFM specifications don't have any examples inside an H3, but it is
+ supported for the GLFM specification.
+
+ ```````````````````````````````` example gitlab
+ Example in an H3
+ .
+ <p>Example in an H3</p>
+ ````````````````````````````````
+
# Second GitLab-Specific Section with Examples
## Strong but with HTML
@@ -142,7 +157,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
`source_specification` will be `gitlab`.
- ```````````````````````````````` example gitlab strong
+ ```````````````````````````````` example gitlab
<strong>
bold
</strong>
@@ -156,7 +171,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
## Strong but skipped
- ```````````````````````````````` example gitlab strong
+ ```````````````````````````````` example gitlab
**this example will be skipped**
.
<p><strong>this example will be skipped</strong></p>
@@ -164,80 +179,151 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
## Strong but manually modified and skipped
- ```````````````````````````````` example gitlab strong
+ ```````````````````````````````` example gitlab
**This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved**
.
<p><strong>This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved</strong></p>
````````````````````````````````
+ # API Request Overrides
+
+ This section contains examples which verify that all of the fixture models which are set up
+ in `render_static_html.rb` are correctly configured. They exercise various `preview_markdown`
+ endpoints via `glfm_example_metadata.yml`.
+
+ ## Group Upload Link
+
+ `preview_markdown` exercising `groups` API endpoint and `UploadLinkFilter`:
+
+ ```````````````````````````````` example gitlab
+ [groups-test-file](/uploads/groups-test-file)
+ .
+ <p><a href="groups-test-file">groups-test-file</a></p>
+ ````````````````````````````````
+
+ ## Project Repo Link
+
+ `preview_markdown` exercising `projects` API endpoint and `RepositoryLinkFilter`:
+
+ ```````````````````````````````` example gitlab
+ [projects-test-file](projects-test-file)
+ .
+ <p><a href="projects-test-file">projects-test-file</a></p>
+ ````````````````````````````````
+
+ ## Project Snippet Ref
+
+ `preview_markdown` exercising `projects` API endpoint and `SnippetReferenceFilter`:
+
+ ```````````````````````````````` example gitlab
+ This project snippet ID reference IS filtered: $88888
+ .
+ <p>This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888">$88888</a>
+ ````````````````````````````````
+
+ ## Personal Snippet Ref
+
+ `preview_markdown` exercising personal (non-project) `snippets` API endpoint. This is
+ only used by the comment field on personal snippets. It has no unique custom markdown
+ extension behavior, and specifically does not render snippet references via
+ `SnippetReferenceFilter`, even if the ID is valid.
+
+ ```````````````````````````````` example gitlab
+ This personal snippet ID reference is NOT filtered: $99999
+ .
+ <p>This personal snippet ID reference is NOT filtered: $99999</p>
+ ````````````````````````````````
+
+ ## Project Wiki Link
+
+ `preview_markdown` exercising project `wikis` API endpoint and `WikiLinkFilter`:
+
+ ```````````````````````````````` example gitlab
+ [project-wikis-test-file](project-wikis-test-file)
+ .
+ <p><a href="project-wikis-test-file">project-wikis-test-file</a></p>
+ ````````````````````````````````
+
<!-- END TESTS -->
# Appendix
Appendix text.
- GLFM_SPEC_TXT_CONTENTS
+ MARKDOWN
end
let(:glfm_example_status_yml_contents) do
- # language=YAML
- <<~GLFM_EXAMPLE_STATUS_YML_CONTENTS
+ <<~YAML
---
- 02_01__inlines__strong__001:
+ 02_01_00__inlines__strong__001:
# The skip_update_example_snapshots key is present, but false, so this example is not skipped
skip_update_example_snapshots: false
- 02_01__inlines__strong__002:
+ 02_01_00__inlines__strong__002:
# It is OK to have an empty (nil) value for an example statuses entry, it means they will all be false.
- 05_01__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
+ 05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
# Always skip this example
skip_update_example_snapshots: 'skipping this example because it is very bad'
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
# Always skip this example, but preserve the existing manual modifications
skip_update_example_snapshots: 'skipping this example because we have manually modified it'
- GLFM_EXAMPLE_STATUS_YML_CONTENTS
+ YAML
+ end
+
+ let(:glfm_example_metadata_yml_contents) do
+ <<~YAML
+ ---
+ 06_01_00__api_request_overrides__group_upload_link__001:
+ api_request_override_path: /groups/glfm_group/preview_markdown
+ 06_02_00__api_request_overrides__project_repo_link__001:
+ api_request_override_path: /glfm_group/glfm_project/preview_markdown
+ 06_03_00__api_request_overrides__project_snippet_ref__001:
+ api_request_override_path: /glfm_group/glfm_project/preview_markdown
+ 06_04_00__api_request_overrides__personal_snippet_ref__001:
+ api_request_override_path: /-/snippets/preview_markdown
+ 06_05_00__api_request_overrides__project_wiki_link__001:
+ api_request_override_path: /glfm_group/glfm_project/-/wikis/new_page/preview_markdown
+ YAML
end
let(:es_html_yml_io_existing_contents) do
- # language=YAML
- <<~ES_HTML_YML_IO_EXISTING_CONTENTS
+ <<~YAML
---
- 00_00__obsolete_entry_to_be_deleted__001:
+ 00_00_00__obsolete_entry_to_be_deleted__001:
canonical: |
- This entry is no longer exists in the spec.txt, and is not skipped, so it will be deleted.
+ This entry is no longer exists in the spec.txt, so it will be deleted.
static: |-
- This entry is no longer exists in the spec.txt, and is not skipped, so it will be deleted.
+ This entry is no longer exists in the spec.txt, so it will be deleted.
wysiwyg: |-
- This entry is no longer exists in the spec.txt, and is not skipped, so it will be deleted.
- 02_01__inlines__strong__001:
+ This entry is no longer exists in the spec.txt, so it will be deleted.
+ 02_01_00__inlines__strong__001:
canonical: |
This entry is existing, but not skipped, so it will be overwritten.
static: |-
This entry is existing, but not skipped, so it will be overwritten.
wysiwyg: |-
This entry is existing, but not skipped, so it will be overwritten.
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
canonical: |
<p><strong>This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved</strong></p>
static: |-
<p>This is the manually modified static HTML which will be preserved</p>
wysiwyg: |-
<p>This is the manually modified WYSIWYG HTML which will be preserved</p>
- ES_HTML_YML_IO_EXISTING_CONTENTS
+ YAML
end
let(:es_prosemirror_json_yml_io_existing_contents) do
- # language=YAML
- <<~ES_PROSEMIRROR_JSON_YML_IO_EXISTING_CONTENTS
+ <<~YAML
---
- 00_00__obsolete_entry_to_be_deleted__001:
+ 00_00_00__obsolete_entry_to_be_deleted__001: |-
{
"obsolete": "This entry is no longer exists in the spec.txt, and is not skipped, so it will be deleted."
}
- 02_01__inlines__strong__001: |-
+ 02_01_00__inlines__strong__001: |-
{
"existing": "This entry is existing, but not skipped, so it will be overwritten."
}
- # 02_01__inlines__strong__002: is omitted from the existing file and skipped, to test that scenario.
- 02_03__inlines__strikethrough_extension__001: |-
+ 02_03_00__inlines__strikethrough_extension__001: |-
{
"type": "doc",
"content": [
@@ -252,15 +338,15 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
}
]
}
- 04_01__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |-
+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |-
{
"existing": "This entry is manually modified and preserved because skip_update_example_snapshot_prosemirror_json will be truthy"
}
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |-
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |-
{
"existing": "This entry is manually modified and preserved because skip_update_example_snapshots will be truthy"
}
- ES_PROSEMIRROR_JSON_YML_IO_EXISTING_CONTENTS
+ YAML
end
before do
@@ -271,6 +357,9 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
# input files
allow(File).to receive(:open).with(glfm_spec_txt_path) { glfm_spec_txt_local_io }
allow(File).to receive(:open).with(glfm_example_status_yml_path) { glfm_example_status_yml_io }
+ allow(File).to receive(:open).with(glfm_example_metadata_yml_path) do
+ glfm_example_metadata_yml_io
+ end
# output files
allow(File).to receive(:open).with(es_examples_index_yml_path, 'w') { es_examples_index_yml_io }
@@ -286,6 +375,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
# Allow normal opening of Tempfile files created during script execution.
tempfile_basenames = [
described_class::MARKDOWN_TEMPFILE_BASENAME[0],
+ described_class::METADATA_TEMPFILE_BASENAME[0],
described_class::STATIC_HTML_TEMPFILE_BASENAME[0],
described_class::WYSIWYG_HTML_AND_JSON_TEMPFILE_BASENAME[0]
].join('|')
@@ -299,40 +389,41 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
allow(subject).to receive(:output)
end
- describe 'when skip_update_example_snapshots is truthy' do
- let(:es_examples_index_yml_contents) { reread_io(es_examples_index_yml_io) }
- let(:es_markdown_yml_contents) { reread_io(es_markdown_yml_io) }
- let(:expected_unskipped_example) do
- /05_01__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001/
- end
-
- it 'still writes the example to examples_index.yml' do
- subject.process(skip_static_and_wysiwyg: true)
+ describe 'glfm_example_status.yml' do
+ describe 'when skip_update_example_snapshots entry is truthy' do
+ let(:es_examples_index_yml_contents) { reread_io(es_examples_index_yml_io) }
+ let(:es_markdown_yml_contents) { reread_io(es_markdown_yml_io) }
+ let(:expected_unskipped_example) do
+ /05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001/
+ end
- expect(es_examples_index_yml_contents).to match(expected_unskipped_example)
- end
+ it 'still writes the example to examples_index.yml' do
+ subject.process(skip_static_and_wysiwyg: true)
- it 'still writes the example to markdown.yml' do
- subject.process(skip_static_and_wysiwyg: true)
+ expect(es_examples_index_yml_contents).to match(expected_unskipped_example)
+ end
- expect(es_markdown_yml_contents).to match(expected_unskipped_example)
- end
+ it 'still writes the example to markdown.yml' do
+ subject.process(skip_static_and_wysiwyg: true)
- describe 'when any other skip_update_example_* is also truthy' do
- let(:glfm_example_status_yml_contents) do
- # language=YAML
- <<~GLFM_EXAMPLE_STATUS_YML_CONTENTS
- ---
- 02_01__inlines__strong__001:
- skip_update_example_snapshots: 'if the skip_update_example_snapshots key is truthy...'
- skip_update_example_snapshot_html_static: '...then no other skip_update_example_* keys can be truthy'
- GLFM_EXAMPLE_STATUS_YML_CONTENTS
+ expect(es_markdown_yml_contents).to match(expected_unskipped_example)
end
- it 'raises an error' do
- expected_msg = "Error: '02_01__inlines__strong__001' must not have any 'skip_update_example_snapshot_*' " \
+ describe 'when any other skip_update_example_snapshot_* is also truthy' do
+ let(:glfm_example_status_yml_contents) do
+ <<~YAML
+ ---
+ 02_01_00__inlines__strong__001:
+ skip_update_example_snapshots: 'if the skip_update_example_snapshots key is truthy...'
+ skip_update_example_snapshot_html_static: '...then no other skip_update_example_* keys can be truthy'
+ YAML
+ end
+
+ it 'raises an error' do
+ expected_msg = "Error: '02_01_00__inlines__strong__001' must not have any 'skip_update_example_snapshot_*' " \
"values specified if 'skip_update_example_snapshots' is truthy"
- expect { subject.process }.to raise_error(/#{Regexp.escape(expected_msg)}/)
+ expect { subject.process }.to raise_error(/#{Regexp.escape(expected_msg)}/)
+ end
end
end
end
@@ -340,31 +431,48 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
describe 'writing examples_index.yml' do
let(:es_examples_index_yml_contents) { reread_io(es_examples_index_yml_io) }
let(:expected_examples_index_yml_contents) do
- # language=YAML
- <<~ES_EXAMPLES_INDEX_YML_CONTENTS
+ <<~YAML
---
- 02_01__inlines__strong__001:
+ 02_01_00__inlines__strong__001:
spec_txt_example_position: 1
source_specification: commonmark
- 02_01__inlines__strong__002:
+ 02_01_00__inlines__strong__002:
spec_txt_example_position: 2
source_specification: github
- 02_03__inlines__strikethrough_extension__001:
+ 02_03_00__inlines__strikethrough_extension__001:
spec_txt_example_position: 4
source_specification: github
- 03_01__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
+ 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
spec_txt_example_position: 5
source_specification: gitlab
- 04_01__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
+ 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001:
spec_txt_example_position: 6
source_specification: gitlab
- 05_01__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
spec_txt_example_position: 7
source_specification: gitlab
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
+ 05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
spec_txt_example_position: 8
source_specification: gitlab
- ES_EXAMPLES_INDEX_YML_CONTENTS
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
+ spec_txt_example_position: 9
+ source_specification: gitlab
+ 06_01_00__api_request_overrides__group_upload_link__001:
+ spec_txt_example_position: 10
+ source_specification: gitlab
+ 06_02_00__api_request_overrides__project_repo_link__001:
+ spec_txt_example_position: 11
+ source_specification: gitlab
+ 06_03_00__api_request_overrides__project_snippet_ref__001:
+ spec_txt_example_position: 12
+ source_specification: gitlab
+ 06_04_00__api_request_overrides__personal_snippet_ref__001:
+ spec_txt_example_position: 13
+ source_specification: gitlab
+ 06_05_00__api_request_overrides__project_wiki_link__001:
+ spec_txt_example_position: 14
+ source_specification: gitlab
+ YAML
end
it 'writes the correct content' do
@@ -377,26 +485,37 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
describe 'writing markdown.yml' do
let(:es_markdown_yml_contents) { reread_io(es_markdown_yml_io) }
let(:expected_markdown_yml_contents) do
- # language=YAML
- <<~ES_MARKDOWN_YML_CONTENTS
+ <<~YAML
---
- 02_01__inlines__strong__001: |
+ 02_01_00__inlines__strong__001: |
__bold__
- 02_01__inlines__strong__002: |
+ 02_01_00__inlines__strong__002: |
__bold with more text__
- 02_03__inlines__strikethrough_extension__001: |
+ 02_03_00__inlines__strikethrough_extension__001: |
~~Hi~~ Hello, world!
- 03_01__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |
+ 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |
**bold**
- 04_01__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |
+ 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: |
+ Example in an H3
+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |
<strong>
bold
</strong>
- 05_01__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001: |
+ 05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001: |
**this example will be skipped**
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |
**This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved**
- ES_MARKDOWN_YML_CONTENTS
+ 06_01_00__api_request_overrides__group_upload_link__001: |
+ [groups-test-file](/uploads/groups-test-file)
+ 06_02_00__api_request_overrides__project_repo_link__001: |
+ [projects-test-file](projects-test-file)
+ 06_03_00__api_request_overrides__project_snippet_ref__001: |
+ This project snippet ID reference IS filtered: $88888
+ 06_04_00__api_request_overrides__personal_snippet_ref__001: |
+ This personal snippet ID reference is NOT filtered: $99999
+ 06_05_00__api_request_overrides__project_wiki_link__001: |
+ [project-wikis-test-file](project-wikis-test-file)
+ YAML
end
it 'writes the correct content' do
@@ -406,6 +525,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
end
end
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
describe 'writing html.yml and prosemirror_json.yml' do
let(:es_html_yml_contents) { reread_io(es_html_yml_io) }
let(:es_prosemirror_json_yml_contents) { reread_io(es_prosemirror_json_yml_io) }
@@ -413,54 +533,64 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
# NOTE: This example_status.yml is crafted in conjunction with expected_html_yml_contents
# to test the behavior of the `skip_update_*` flags
let(:glfm_example_status_yml_contents) do
- # language=YAML
- <<~GLFM_EXAMPLE_STATUS_YML_CONTENTS
+ <<~YAML
---
- 02_01__inlines__strong__002:
+ 02_01_00__inlines__strong__002:
+ # NOTE: 02_01_00__inlines__strong__002: is omitted from the existing prosemirror_json.yml file, and is also
+ # skipped here, to show that an example does not need to exist in order to be skipped.
+ # TODO: This should be changed to raise an error instead, to enforce that there cannot be orphaned
+ # entries in glfm_example_status.yml. This task is captured in
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/361241#other-cleanup-tasks
skip_update_example_snapshot_prosemirror_json: "skipping because JSON isn't cool enough"
- 03_01__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
+ 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
skip_update_example_snapshot_html_static: "skipping because there's too much static"
- 04_01__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
skip_update_example_snapshot_html_wysiwyg: 'skipping because what you see is NOT what you get'
skip_update_example_snapshot_prosemirror_json: "skipping because JSON still isn't cool enough"
- 05_01__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
+ 05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
skip_update_example_snapshots: 'skipping this example because it is very bad'
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
skip_update_example_snapshots: 'skipping this example because we have manually modified it'
- GLFM_EXAMPLE_STATUS_YML_CONTENTS
+ YAML
end
let(:expected_html_yml_contents) do
- # language=YAML
- <<~ES_HTML_YML_CONTENTS
+ <<~YAML
---
- 02_01__inlines__strong__001:
+ 02_01_00__inlines__strong__001:
canonical: |
<p><strong>bold</strong></p>
static: |-
<p data-sourcepos="1:1-1:8" dir="auto"><strong>bold</strong></p>
wysiwyg: |-
<p><strong>bold</strong></p>
- 02_01__inlines__strong__002:
+ 02_01_00__inlines__strong__002:
canonical: |
<p><strong>bold with more text</strong></p>
static: |-
<p data-sourcepos="1:1-1:23" dir="auto"><strong>bold with more text</strong></p>
wysiwyg: |-
<p><strong>bold with more text</strong></p>
- 02_03__inlines__strikethrough_extension__001:
+ 02_03_00__inlines__strikethrough_extension__001:
canonical: |
<p><del>Hi</del> Hello, world!</p>
static: |-
<p data-sourcepos="1:1-1:20" dir="auto"><del>Hi</del> Hello, world!</p>
wysiwyg: |-
<p><s>Hi</s> Hello, world!</p>
- 03_01__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
+ 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
canonical: |
<p><strong>bold</strong></p>
wysiwyg: |-
<p><strong>bold</strong></p>
- 04_01__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
+ 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001:
+ canonical: |
+ <p>Example in an H3</p>
+ static: |-
+ <p data-sourcepos="1:1-1:16" dir="auto">Example in an H3</p>
+ wysiwyg: |-
+ <p>Example in an H3</p>
+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
canonical: |
<p><strong>
bold
@@ -469,21 +599,55 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
<strong>
bold
</strong>
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
canonical: |
<p><strong>This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved</strong></p>
static: |-
<p>This is the manually modified static HTML which will be preserved</p>
wysiwyg: |-
<p>This is the manually modified WYSIWYG HTML which will be preserved</p>
- ES_HTML_YML_CONTENTS
+ 06_01_00__api_request_overrides__group_upload_link__001:
+ canonical: |
+ <p><a href="groups-test-file">groups-test-file</a></p>
+ static: |-
+ <p data-sourcepos="1:1-1:45" dir="auto"><a href="/groups/glfm_group/-/uploads/groups-test-file" data-canonical-src="/uploads/groups-test-file" data-link="true" class="gfm">groups-test-file</a></p>
+ wysiwyg: |-
+ <p><a target="_blank" rel="noopener noreferrer nofollow" href="/uploads/groups-test-file">groups-test-file</a></p>
+ 06_02_00__api_request_overrides__project_repo_link__001:
+ canonical: |
+ <p><a href="projects-test-file">projects-test-file</a></p>
+ static: |-
+ <p data-sourcepos="1:1-1:40" dir="auto"><a href="/glfm_group/glfm_project/-/blob/master/projects-test-file">projects-test-file</a></p>
+ wysiwyg: |-
+ <p><a target="_blank" rel="noopener noreferrer nofollow" href="projects-test-file">projects-test-file</a></p>
+ 06_03_00__api_request_overrides__project_snippet_ref__001:
+ canonical: |
+ <p>This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888">$88888</a>
+ static: |-
+ <p data-sourcepos="1:1-1:53" dir="auto">This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888" data-reference-type="snippet" data-original="$88888" data-link="false" data-link-reference="false" data-project="77777" data-snippet="88888" data-container="body" data-placement="top" title="glfm_project_snippet" class="gfm gfm-snippet has-tooltip">$88888</a></p>
+ wysiwyg: |-
+ <p>This project snippet ID reference IS filtered: $88888</p>
+ 06_04_00__api_request_overrides__personal_snippet_ref__001:
+ canonical: |
+ <p>This personal snippet ID reference is NOT filtered: $99999</p>
+ static: |-
+ <p data-sourcepos="1:1-1:58" dir="auto">This personal snippet ID reference is NOT filtered: $99999</p>
+ wysiwyg: |-
+ <p>This personal snippet ID reference is NOT filtered: $99999</p>
+ 06_05_00__api_request_overrides__project_wiki_link__001:
+ canonical: |
+ <p><a href="project-wikis-test-file">project-wikis-test-file</a></p>
+ static: |-
+ <p data-sourcepos="1:1-1:50" dir="auto"><a href="/glfm_group/glfm_project/-/wikis/project-wikis-test-file" data-canonical-src="project-wikis-test-file">project-wikis-test-file</a></p>
+ wysiwyg: |-
+ <p><a target="_blank" rel="noopener noreferrer nofollow" href="project-wikis-test-file">project-wikis-test-file</a></p>
+ YAML
end
let(:expected_prosemirror_json_contents) do
- # language=YAML
- <<~ES_PROSEMIRROR_JSON_YML_CONTENTS
+ <<~YAML
---
- 02_01__inlines__strong__001: |-
+ 02_01_00__inlines__strong__001: |-
{
"type": "doc",
"content": [
@@ -503,7 +667,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
}
]
}
- 02_03__inlines__strikethrough_extension__001: |-
+ 02_03_00__inlines__strikethrough_extension__001: |-
{
"type": "doc",
"content": [
@@ -527,7 +691,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
}
]
}
- 03_01__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |-
+ 03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |-
{
"type": "doc",
"content": [
@@ -547,15 +711,144 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
}
]
}
- 04_01__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |-
+ 03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "Example in an H3"
+ }
+ ]
+ }
+ ]
+ }
+ 04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |-
{
"existing": "This entry is manually modified and preserved because skip_update_example_snapshot_prosemirror_json will be truthy"
}
- 05_02__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |-
+ 05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |-
{
"existing": "This entry is manually modified and preserved because skip_update_example_snapshots will be truthy"
}
- ES_PROSEMIRROR_JSON_YML_CONTENTS
+ 06_01_00__api_request_overrides__group_upload_link__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "href": "/uploads/groups-test-file",
+ "target": "_blank",
+ "class": null,
+ "title": null,
+ "canonicalSrc": "/uploads/groups-test-file",
+ "isReference": false
+ }
+ }
+ ],
+ "text": "groups-test-file"
+ }
+ ]
+ }
+ ]
+ }
+ 06_02_00__api_request_overrides__project_repo_link__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "href": "projects-test-file",
+ "target": "_blank",
+ "class": null,
+ "title": null,
+ "canonicalSrc": "projects-test-file",
+ "isReference": false
+ }
+ }
+ ],
+ "text": "projects-test-file"
+ }
+ ]
+ }
+ ]
+ }
+ 06_03_00__api_request_overrides__project_snippet_ref__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This project snippet ID reference IS filtered: $88888"
+ }
+ ]
+ }
+ ]
+ }
+ 06_04_00__api_request_overrides__personal_snippet_ref__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This personal snippet ID reference is NOT filtered: $99999"
+ }
+ ]
+ }
+ ]
+ }
+ 06_05_00__api_request_overrides__project_wiki_link__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "href": "project-wikis-test-file",
+ "target": "_blank",
+ "class": null,
+ "title": null,
+ "canonicalSrc": "project-wikis-test-file",
+ "isReference": false
+ }
+ }
+ ],
+ "text": "project-wikis-test-file"
+ }
+ ]
+ }
+ ]
+ }
+ YAML
end
before do
@@ -581,6 +874,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
expect(es_prosemirror_json_yml_contents).to eq(expected_prosemirror_json_contents)
end
end
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
def reread_io(io)
# Reset the io StringIO to the beginning position of the buffer
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index e8d34b13efa..9fb671e0016 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
let(:glfm_spec_txt_io) { StringIO.new }
let(:ghfm_spec_txt_contents) do
- <<~GHFM_SPEC_TXT_CONTENTS
+ <<~MARKDOWN
---
title: GitHub Flavored Markdown Spec
version: 0.29
@@ -49,25 +49,26 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
# Appendix
Appendix text.
- GHFM_SPEC_TXT_CONTENTS
+ MARKDOWN
end
let(:glfm_intro_txt_contents) do
- <<~GLFM_INTRO_TXT_CONTENTS
+ # language=Markdown
+ <<~MARKDOWN
# Introduction
## What is GitLab Flavored Markdown?
Intro text about GitLab Flavored Markdown.
- GLFM_INTRO_TXT_CONTENTS
+ MARKDOWN
end
let(:glfm_examples_txt_contents) do
- <<~GLFM_EXAMPLES_TXT_CONTENTS
+ <<~MARKDOWN
# GitLab-Specific Section with Examples
Some examples.
- GLFM_EXAMPLES_TXT_CONTENTS
+ MARKDOWN
end
before do
@@ -118,12 +119,12 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
context 'with error handling' do
context 'with a version mismatch' do
let(:ghfm_spec_txt_contents) do
- <<~GHFM_SPEC_TXT_CONTENTS
+ <<~MARKDOWN
---
title: GitHub Flavored Markdown Spec
version: 0.30
...
- GHFM_SPEC_TXT_CONTENTS
+ MARKDOWN
end
it 'raises an error' do
@@ -173,7 +174,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
end
it 'inserts the GitLab examples sections before the appendix section' do
- expected = <<~GHFM_SPEC_TXT_CONTENTS
+ expected = <<~MARKDOWN
End of last GitHub examples section.
# GitLab-Specific Section with Examples
@@ -183,7 +184,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
<!-- END TESTS -->
# Appendix
- GHFM_SPEC_TXT_CONTENTS
+ MARKDOWN
expect(glfm_contents).to match(/#{Regexp.escape(expected)}/m)
end
end
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index d0f1d3dc41b..46023d5823d 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -195,33 +195,13 @@ RSpec.describe Trigger do
end
end
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do
+ context 'when CI_COMMIT_SHA is set' do
before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha')
- end
-
- it 'sets TOP_UPSTREAM_SOURCE_SHA to ci_merge_request_source_branch_sha' do
- expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq('ci_merge_request_source_branch_sha')
- end
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
- end
-
- it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do
- expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA'])
- end
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
+ stub_env('CI_COMMIT_SHA', 'ci_commit_sha')
end
it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do
- expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA'])
+ expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq('ci_commit_sha')
end
end
end
@@ -264,195 +244,6 @@ RSpec.describe Trigger do
end
end
- describe Trigger::Omnibus do
- describe '#variables' do
- it 'invokes the trigger with expected variables' do
- expect(subject.variables).to include(
- 'QA_IMAGE' => env['QA_IMAGE'],
- 'SKIP_QA_DOCKER' => 'true',
- 'ALTERNATIVE_SOURCES' => 'true',
- 'CACHE_UPDATE' => env['OMNIBUS_GITLAB_CACHE_UPDATE'],
- 'GITLAB_QA_OPTIONS' => env['GITLAB_QA_OPTIONS'],
- 'QA_TESTS' => env['QA_TESTS'],
- 'ALLURE_JOB_NAME' => env['ALLURE_JOB_NAME']
- )
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha')
- end
-
- it 'sets GITLAB_VERSION & IMAGE_TAG to ci_merge_request_source_branch_sha' do
- expect(subject.variables).to include(
- 'GITLAB_VERSION' => 'ci_merge_request_source_branch_sha',
- 'IMAGE_TAG' => 'ci_merge_request_source_branch_sha'
- )
- end
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
- end
-
- it 'sets GITLAB_VERSION & IMAGE_TAG to CI_COMMIT_SHA' do
- expect(subject.variables).to include(
- 'GITLAB_VERSION' => env['CI_COMMIT_SHA'],
- 'IMAGE_TAG' => env['CI_COMMIT_SHA']
- )
- end
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
- end
-
- it 'sets GITLAB_VERSION & IMAGE_TAG to CI_COMMIT_SHA' do
- expect(subject.variables).to include(
- 'GITLAB_VERSION' => env['CI_COMMIT_SHA'],
- 'IMAGE_TAG' => env['CI_COMMIT_SHA']
- )
- end
- end
-
- context 'when Trigger.security? is true' do
- before do
- allow(Trigger).to receive(:security?).and_return(true)
- end
-
- it 'sets SECURITY_SOURCES to true' do
- expect(subject.variables['SECURITY_SOURCES']).to eq('true')
- end
- end
-
- context 'when Trigger.security? is false' do
- before do
- allow(Trigger).to receive(:security?).and_return(false)
- end
-
- it 'sets SECURITY_SOURCES to false' do
- expect(subject.variables['SECURITY_SOURCES']).to eq('false')
- end
- end
-
- context 'when Trigger.ee? is true' do
- before do
- allow(Trigger).to receive(:ee?).and_return(true)
- end
-
- it 'sets ee to true' do
- expect(subject.variables['ee']).to eq('true')
- end
- end
-
- context 'when Trigger.ee? is false' do
- before do
- allow(Trigger).to receive(:ee?).and_return(false)
- end
-
- it 'sets ee to false' do
- expect(subject.variables['ee']).to eq('false')
- end
- end
-
- context 'when QA_BRANCH is set' do
- before do
- stub_env('QA_BRANCH', 'qa_branch')
- end
-
- it 'sets QA_BRANCH to qa_branch' do
- expect(subject.variables['QA_BRANCH']).to eq('qa_branch')
- end
- end
- end
-
- describe '.access_token' do
- context 'when OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN is set' do
- let(:omnibus_gitlab_project_access_token) { 'omnibus_gitlab_project_access_token' }
-
- before do
- stub_env('OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN', omnibus_gitlab_project_access_token)
- end
-
- it 'returns the omnibus-specific access token' do
- expect(described_class.access_token).to eq(omnibus_gitlab_project_access_token)
- end
- end
-
- context 'when OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN is not set' do
- before do
- stub_env('OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN', nil)
- end
-
- it 'returns the default access token' do
- expect(described_class.access_token).to eq(Trigger::Base.access_token)
- end
- end
- end
-
- describe '#invoke!' do
- let(:downstream_project_path) { 'gitlab-org/build/omnibus-gitlab-mirror' }
- let(:ref) { 'master' }
-
- let(:env) do
- super().merge(
- 'QA_IMAGE' => 'qa_image',
- 'GITLAB_QA_OPTIONS' => 'gitlab_qa_options',
- 'QA_TESTS' => 'qa_tests',
- 'ALLURE_JOB_NAME' => 'allure_job_name'
- )
- end
-
- describe '#downstream_project_path' do
- context 'when OMNIBUS_PROJECT_PATH is set' do
- let(:downstream_project_path) { 'omnibus_project_path' }
-
- before do
- stub_env('OMNIBUS_PROJECT_PATH', downstream_project_path)
- end
-
- it 'triggers the pipeline on the correct project' do
- expect_run_trigger_with_params
-
- subject.invoke!
- end
- end
- end
-
- describe '#ref' do
- context 'when OMNIBUS_BRANCH is set' do
- let(:ref) { 'omnibus_branch' }
-
- before do
- stub_env('OMNIBUS_BRANCH', ref)
- end
-
- it 'triggers the pipeline on the correct ref' do
- expect_run_trigger_with_params
-
- subject.invoke!
- end
- end
- end
-
- context 'when CI_COMMIT_REF_NAME is a stable branch' do
- let(:ref) { '14-10-stable' }
-
- before do
- stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee")
- end
-
- it 'triggers the pipeline on the correct ref' do
- expect_run_trigger_with_params
-
- subject.invoke!
- end
- end
- end
- end
-
describe Trigger::CNG do
describe '#variables' do
it 'does not include redundant variables' do
@@ -496,33 +287,13 @@ RSpec.describe Trigger do
end
describe "GITLAB_VERSION" do
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha')
- end
-
- it 'sets GITLAB_VERSION to CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' do
- expect(subject.variables['GITLAB_VERSION']).to eq('ci_merge_request_source_branch_sha')
- end
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do
+ context 'when CI_COMMIT_SHA is set' do
before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
+ stub_env('CI_COMMIT_SHA', 'ci_commit_sha')
end
it 'sets GITLAB_VERSION to CI_COMMIT_SHA' do
- expect(subject.variables['GITLAB_VERSION']).to eq(env['CI_COMMIT_SHA'])
- end
- end
-
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
- end
-
- it 'sets GITLAB_VERSION to CI_COMMIT_SHA' do
- expect(subject.variables['GITLAB_VERSION']).to eq(env['CI_COMMIT_SHA'])
+ expect(subject.variables['GITLAB_VERSION']).to eq('ci_commit_sha')
end
end
end
@@ -560,10 +331,9 @@ RSpec.describe Trigger do
end
end
- context 'when CI_COMMIT_TAG and CI_MERGE_REQUEST_SOURCE_BRANCH_SHA are nil' do
+ context 'when CI_COMMIT_TAG is nil' do
before do
stub_env('CI_COMMIT_TAG', nil)
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
end
it 'sets GITLAB_ASSETS_TAG to CI_COMMIT_SHA' do
@@ -841,35 +611,33 @@ RSpec.describe Trigger do
expect(subject.variables).to include('TRIGGERED_USER_LOGIN' => env['GITLAB_USER_LOGIN'])
end
- describe "GITLAB_COMMIT_SHA" do
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha')
- end
+ context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do
+ before do
+ stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha')
+ end
- it 'sets GITLAB_COMMIT_SHA to ci_merge_request_source_branch_sha' do
- expect(subject.variables['GITLAB_COMMIT_SHA']).to eq('ci_merge_request_source_branch_sha')
- end
+ it 'sets TOP_UPSTREAM_SOURCE_SHA to ci_merge_request_source_branch_sha' do
+ expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq('ci_merge_request_source_branch_sha')
end
+ end
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
- end
+ context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do
+ before do
+ stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
+ end
- it 'sets GITLAB_COMMIT_SHA to CI_COMMIT_SHA' do
- expect(subject.variables['GITLAB_COMMIT_SHA']).to eq(env['CI_COMMIT_SHA'])
- end
+ it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do
+ expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA'])
end
+ end
- context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do
- before do
- stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
- end
+ context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do
+ before do
+ stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
+ end
- it 'sets GITLAB_COMMIT_SHA to CI_COMMIT_SHA' do
- expect(subject.variables['GITLAB_COMMIT_SHA']).to eq(env['CI_COMMIT_SHA'])
- end
+ it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do
+ expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA'])
end
end
end
diff --git a/spec/serializers/access_token_entity_base_spec.rb b/spec/serializers/access_token_entity_base_spec.rb
new file mode 100644
index 00000000000..e14a07a346a
--- /dev/null
+++ b/spec/serializers/access_token_entity_base_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AccessTokenEntityBase do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:token) { create(:personal_access_token, user: user, expires_at: nil) }
+
+ subject(:json) { described_class.new(token).as_json }
+
+ it 'has the correct attributes' do
+ expect(json).to(
+ include(
+ id: token.id,
+ name: token.name,
+ revoked: false,
+ created_at: token.created_at,
+ scopes: token.scopes,
+ expires_at: nil,
+ expired: false,
+ expires_soon: false
+ ))
+
+ expect(json).not_to include(:token)
+ end
+end
diff --git a/spec/serializers/ci/dag_pipeline_entity_spec.rb b/spec/serializers/ci/dag_pipeline_entity_spec.rb
index 548fd247743..a8ac76d800f 100644
--- a/spec/serializers/ci/dag_pipeline_entity_spec.rb
+++ b/spec/serializers/ci/dag_pipeline_entity_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Ci::DagPipelineEntity do
let_it_be(:request) { double(:request) }
- let(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
let(:entity) { described_class.new(pipeline, request: request) }
describe '#as_json' do
@@ -28,9 +29,13 @@ RSpec.describe Ci::DagPipelineEntity do
end
context 'when pipeline has jobs' do
- let!(:build_job) { create(:ci_build, stage: 'build', pipeline: pipeline) }
- let!(:test_job) { create(:ci_build, stage: 'test', pipeline: pipeline) }
- let!(:deploy_job) { create(:ci_build, stage: 'deploy', pipeline: pipeline) }
+ let_it_be(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+ let_it_be(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
+ let_it_be(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
+
+ let!(:build_job) { create(:ci_build, ci_stage: build_stage, pipeline: pipeline) }
+ let!(:test_job) { create(:ci_build, ci_stage: test_stage, pipeline: pipeline) }
+ let!(:deploy_job) { create(:ci_build, ci_stage: deploy_stage, pipeline: pipeline) }
it 'contains 3 stages' do
stages = subject[:stages]
@@ -47,28 +52,31 @@ RSpec.describe Ci::DagPipelineEntity do
let!(:stage_test) { create(:ci_stage, name: 'test', position: 2, pipeline: pipeline) }
let!(:stage_deploy) { create(:ci_stage, name: 'deploy', position: 3, pipeline: pipeline) }
- let!(:job_build_1) { create(:ci_build, name: 'build 1', stage: 'build', pipeline: pipeline) }
- let!(:job_build_2) { create(:ci_build, name: 'build 2', stage: 'build', pipeline: pipeline) }
- let!(:commit_status) { create(:generic_commit_status, stage: 'build', pipeline: pipeline) }
+ let!(:job_build_1) { create(:ci_build, name: 'build 1', ci_stage: stage_build, pipeline: pipeline) }
+ let!(:job_build_2) { create(:ci_build, name: 'build 2', ci_stage: stage_build, pipeline: pipeline) }
+ let!(:commit_status) { create(:generic_commit_status, ci_stage: stage_build, pipeline: pipeline) }
- let!(:job_rspec_1) { create(:ci_build, name: 'rspec 1/2', stage: 'test', pipeline: pipeline) }
- let!(:job_rspec_2) { create(:ci_build, name: 'rspec 2/2', stage: 'test', pipeline: pipeline) }
+ let!(:job_rspec_1) { create(:ci_build, name: 'rspec 1/2', ci_stage: stage_test, pipeline: pipeline) }
+ let!(:job_rspec_2) { create(:ci_build, name: 'rspec 2/2', ci_stage: stage_test, pipeline: pipeline) }
let!(:job_jest) do
- create(:ci_build, name: 'jest', stage: 'test', scheduling_type: 'dag', pipeline: pipeline).tap do |job|
+ create(:ci_build, name: 'jest', ci_stage: stage_test, scheduling_type: 'dag', pipeline: pipeline)
+ .tap do |job|
create(:ci_build_need, name: 'build 1', build: job)
end
end
let!(:job_deploy_ruby) do
- create(:ci_build, name: 'deploy_ruby', stage: 'deploy', scheduling_type: 'dag', pipeline: pipeline).tap do |job|
+ create(:ci_build, name: 'deploy_ruby', ci_stage: stage_deploy, scheduling_type: 'dag', pipeline: pipeline)
+ .tap do |job|
create(:ci_build_need, name: 'rspec 1/2', build: job)
create(:ci_build_need, name: 'rspec 2/2', build: job)
end
end
let!(:job_deploy_js) do
- create(:ci_build, name: 'deploy_js', stage: 'deploy', scheduling_type: 'dag', pipeline: pipeline).tap do |job|
+ create(:ci_build, name: 'deploy_js', ci_stage: stage_deploy, scheduling_type: 'dag', pipeline: pipeline)
+ .tap do |job|
create(:ci_build_need, name: 'jest', build: job)
end
end
diff --git a/spec/serializers/ci/lint/job_entity_spec.rb b/spec/serializers/ci/lint/job_entity_spec.rb
index 2ef86cfd004..e1477612ad5 100644
--- a/spec/serializers/ci/lint/job_entity_spec.rb
+++ b/spec/serializers/ci/lint/job_entity_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Ci::Lint::JobEntity, :aggregate_failures do
stage: 'test',
before_script: ['bundle install', 'bundle exec rake db:create'],
script: ["rake spec"],
- after_script: ["rake spec"],
+ after_script: ["rake spec"],
tag_list: %w[ruby postgres],
environment: { name: 'hello', url: 'world' },
when: 'on_success',
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 7c4c146575d..2de27deeffe 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe ClusterEntity do
context 'when no application has been installed' do
let(:cluster) { create(:cluster, :instance) }
- subject { described_class.new(cluster, request: request).as_json[:applications]}
+ subject { described_class.new(cluster, request: request).as_json[:applications] }
it 'contains helm as not_installable' do
expect(subject).not_to be_empty
diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb
index 9ea00bc79e1..00e6a26d0be 100644
--- a/spec/serializers/container_repository_entity_spec.rb
+++ b/spec/serializers/container_repository_entity_spec.rb
@@ -14,8 +14,7 @@ RSpec.describe ContainerRepositoryEntity do
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags(repository: :any,
- tags: %w[stable latest])
+ stub_container_registry_tags(repository: :any, tags: %w[stable latest])
allow(request).to receive(:project).and_return(project)
allow(request).to receive(:current_user).and_return(user)
end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index a017f7523e9..433ce344680 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -61,8 +61,7 @@ RSpec.describe DeploymentEntity do
context 'when the pipeline has another manual action' do
let!(:other_build) do
- create(:ci_build, :manual, name: 'another deploy',
- pipeline: pipeline, environment: build.environment)
+ create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline, environment: build.environment)
end
let!(:other_deployment) { create(:deployment, deployable: build) }
diff --git a/spec/serializers/group_access_token_entity_spec.rb b/spec/serializers/group_access_token_entity_spec.rb
index 39b587c7df7..05609dc3c7a 100644
--- a/spec/serializers/group_access_token_entity_spec.rb
+++ b/spec/serializers/group_access_token_entity_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe GroupAccessTokenEntity do
scopes: token.scopes,
user_id: token.user_id,
revoke_path: expected_revoke_path,
- access_level: ::Gitlab::Access::DEVELOPER
+ role: 'Developer'
))
expect(json).not_to include(:token)
@@ -48,7 +48,7 @@ RSpec.describe GroupAccessTokenEntity do
scopes: token.scopes,
user_id: token.user_id,
revoke_path: expected_revoke_path,
- access_level: nil
+ role: nil
))
expect(json).not_to include(:token)
diff --git a/spec/serializers/impersonation_access_token_entity_spec.rb b/spec/serializers/impersonation_access_token_entity_spec.rb
new file mode 100644
index 00000000000..e8517779c0d
--- /dev/null
+++ b/spec/serializers/impersonation_access_token_entity_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe ImpersonationAccessTokenEntity do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:token) { create(:personal_access_token, :impersonation, user: user) }
+
+ subject(:json) { described_class.new(token).as_json }
+
+ it 'has the correct attributes' do
+ expected_revoke_path = Gitlab::Routing.url_helpers
+ .revoke_admin_user_impersonation_token_path(
+ { user_id: user, id: token })
+
+ expect(json).to(
+ include(
+ id: token.id,
+ name: token.name,
+ scopes: token.scopes,
+ user_id: token.user_id,
+ revoke_path: expected_revoke_path
+ ))
+
+ expect(json).not_to include(:token)
+ end
+end
diff --git a/spec/serializers/impersonation_access_token_serializer_spec.rb b/spec/serializers/impersonation_access_token_serializer_spec.rb
new file mode 100644
index 00000000000..0c8cebb94b1
--- /dev/null
+++ b/spec/serializers/impersonation_access_token_serializer_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe ImpersonationAccessTokenSerializer do
+ subject(:serializer) { described_class.new }
+
+ describe '#represent' do
+ it 'can render a single token' do
+ token = create(:personal_access_token)
+
+ expect(serializer.represent(token)).to be_kind_of(Hash)
+ end
+
+ it 'can render a collection of tokens' do
+ tokens = create_list(:personal_access_token, 2)
+
+ expect(serializer.represent(tokens)).to be_kind_of(Array)
+ end
+ end
+end
diff --git a/spec/serializers/import/provider_repo_serializer_spec.rb b/spec/serializers/import/provider_repo_serializer_spec.rb
index 430bad151d3..905685c75e3 100644
--- a/spec/serializers/import/provider_repo_serializer_spec.rb
+++ b/spec/serializers/import/provider_repo_serializer_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Import::ProviderRepoSerializer do
end
it 'raises an error if invalid provider supplied' do
- expect { described_class.new.represent({}, { provider: :invalid })}.to raise_error { NotImplementedError }
+ expect { described_class.new.represent({}, { provider: :invalid }) }.to raise_error { NotImplementedError }
end
end
end
diff --git a/spec/serializers/member_user_entity_spec.rb b/spec/serializers/member_user_entity_spec.rb
index 4dd6848c47b..638347738f2 100644
--- a/spec/serializers/member_user_entity_spec.rb
+++ b/spec/serializers/member_user_entity_spec.rb
@@ -27,6 +27,12 @@ RSpec.describe MemberUserEntity do
expect(entity_hash[:blocked]).to be(true)
end
+ it 'correctly exposes `is_bot`' do
+ allow(user).to receive(:bot?).and_return(true)
+
+ expect(entity_hash[:is_bot]).to be(true)
+ end
+
it 'does not expose `two_factor_enabled` by default' do
expect(entity_hash[:two_factor_enabled]).to be(nil)
end
diff --git a/spec/serializers/merge_request_metrics_helper_spec.rb b/spec/serializers/merge_request_metrics_helper_spec.rb
index 8f683df1faa..ec764bf7853 100644
--- a/spec/serializers/merge_request_metrics_helper_spec.rb
+++ b/spec/serializers/merge_request_metrics_helper_spec.rb
@@ -57,9 +57,9 @@ RSpec.describe MergeRequestMetricsHelper do
expect(MergeRequest::Metrics).to receive(:new)
.with(latest_closed_at: closed_event&.updated_at,
- latest_closed_by: closed_event&.author,
- merged_at: merge_event&.updated_at,
- merged_by: merge_event&.author)
+ latest_closed_by: closed_event&.author,
+ merged_at: merge_event&.updated_at,
+ merged_by: merge_event&.author)
.and_call_original
subject
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index 67f8860ed4a..ca38721cc7f 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe PipelineDetailsEntity do
let(:pipeline) { create(:ci_empty_pipeline) }
before do
- create(:generic_commit_status, pipeline: pipeline)
+ create(:commit_status, pipeline: pipeline)
end
it 'contains stages' do
diff --git a/spec/serializers/project_access_token_entity_spec.rb b/spec/serializers/project_access_token_entity_spec.rb
index 616aa45e9d5..4b5b4d4d77d 100644
--- a/spec/serializers/project_access_token_entity_spec.rb
+++ b/spec/serializers/project_access_token_entity_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe ProjectAccessTokenEntity do
scopes: token.scopes,
user_id: token.user_id,
revoke_path: expected_revoke_path,
- access_level: ::Gitlab::Access::DEVELOPER
+ role: 'Developer'
))
expect(json).not_to include(:token)
@@ -52,7 +52,7 @@ RSpec.describe ProjectAccessTokenEntity do
scopes: token.scopes,
user_id: token.user_id,
revoke_path: expected_revoke_path,
- access_level: nil
+ role: nil
))
expect(json).not_to include(:token)
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 6f28f892f00..73d185283b6 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -217,18 +217,20 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
let(:ref) { mr_merge_if_green_enabled.source_branch }
let(:sha) { project.commit(ref).id }
+ let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+
let(:pipeline) do
create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
end
let!(:build) do
create(:ci_build, :created, pipeline: pipeline, ref: ref,
- name: 'build', stage: 'build')
+ name: 'build', ci_stage: build_stage )
end
let!(:test) do
create(:ci_build, :created, pipeline: pipeline, ref: ref,
- name: 'test', stage: 'test')
+ name: 'test')
end
before do
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index 81229cc8431..ec9cc719e24 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -277,7 +277,7 @@ RSpec.describe BulkImports::FileDownloadService do
let_it_be(:content_disposition) { 'filename="../../xxx.b"' }
before do
- stub_const("#{described_class}::FILENAME_SIZE_LIMIT", 1)
+ stub_const('BulkImports::FileDownloads::FilenameFetch::FILENAME_SIZE_LIMIT', 1)
end
it 'raises an error when the filename is not provided in the request header' do
diff --git a/spec/services/bulk_imports/tree_export_service_spec.rb b/spec/services/bulk_imports/tree_export_service_spec.rb
index ffb81fe2b5f..6e26cb6dc2b 100644
--- a/spec/services/bulk_imports/tree_export_service_spec.rb
+++ b/spec/services/bulk_imports/tree_export_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe BulkImports::TreeExportService do
let(:relation) { 'issues' }
- subject(:service) { described_class.new(project, export_path, relation) }
+ subject(:service) { described_class.new(project, export_path, relation, project.owner) }
describe '#execute' do
it 'executes export service and archives exported data' do
@@ -21,7 +21,7 @@ RSpec.describe BulkImports::TreeExportService do
context 'when unsupported relation is passed' do
it 'raises an error' do
- service = described_class.new(project, export_path, 'unsupported')
+ service = described_class.new(project, export_path, 'unsupported', project.owner)
expect { service.execute }.to raise_error(BulkImports::Error, 'Unsupported relation export type')
end
diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/after_requeue_job_service_spec.rb
index fb67ee18fb2..1f692bdb71a 100644
--- a/spec/services/ci/after_requeue_job_service_spec.rb
+++ b/spec/services/ci/after_requeue_job_service_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
check_jobs_statuses(
a1: 'pending',
a2: 'created',
- a3: 'skipped',
+ a3: 'created',
b1: 'success',
b2: 'created',
c1: 'created',
@@ -120,6 +120,26 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
)
end
+ context 'when the FF ci_requeue_with_dag_object_hierarchy is disabled' do
+ before do
+ stub_feature_flags(ci_requeue_with_dag_object_hierarchy: false)
+ end
+
+ it 'marks subsequent skipped jobs as processable but leaves a3 created' do
+ execute_after_requeue_service(a1)
+
+ check_jobs_statuses(
+ a1: 'pending',
+ a2: 'created',
+ a3: 'skipped',
+ b1: 'success',
+ b2: 'created',
+ c1: 'created',
+ c2: 'created'
+ )
+ end
+ end
+
context 'when executed by a different user than the original owner' do
let(:retryer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:service) { described_class.new(project, retryer) }
@@ -140,7 +160,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
expect(jobs_name_status_owner_needs).to contain_exactly(
{ 'name' => 'a1', 'status' => 'pending', 'user_id' => user.id, 'needs' => [] },
{ 'name' => 'a2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a1'] },
- { 'name' => 'a3', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a2'] },
+ { 'name' => 'a3', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a2'] },
{ 'name' => 'b1', 'status' => 'success', 'user_id' => user.id, 'needs' => [] },
{ 'name' => 'b2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a2'] },
{ 'name' => 'c1', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['b2'] },
@@ -237,6 +257,79 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
end
end
+ context 'with same-stage needs' do
+ let(:config) do
+ <<-EOY
+ a:
+ script: exit $(($RANDOM % 2))
+
+ b:
+ script: exit 0
+ needs: [a]
+
+ c:
+ script: exit 0
+ needs: [b]
+ EOY
+ end
+
+ let(:pipeline) do
+ Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
+ end
+
+ let(:a) { find_job('a') }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ check_jobs_statuses(
+ a: 'pending',
+ b: 'created',
+ c: 'created'
+ )
+
+ a.drop!
+ check_jobs_statuses(
+ a: 'failed',
+ b: 'skipped',
+ c: 'skipped'
+ )
+
+ new_a = Ci::RetryJobService.new(project, user).clone!(a)
+ new_a.enqueue!
+ check_jobs_statuses(
+ a: 'pending',
+ b: 'skipped',
+ c: 'skipped'
+ )
+ end
+
+ it 'marks subsequent skipped jobs as processable' do
+ execute_after_requeue_service(a)
+
+ check_jobs_statuses(
+ a: 'pending',
+ b: 'created',
+ c: 'created'
+ )
+ end
+
+ context 'when the FF ci_requeue_with_dag_object_hierarchy is disabled' do
+ before do
+ stub_feature_flags(ci_requeue_with_dag_object_hierarchy: false)
+ end
+
+ it 'marks the next subsequent skipped job as processable but leaves c skipped' do
+ execute_after_requeue_service(a)
+
+ check_jobs_statuses(
+ a: 'pending',
+ b: 'created',
+ c: 'skipped'
+ )
+ end
+ end
+ end
+
private
def find_job(name)
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
index bf2e5302d2e..359ea0699e4 100644
--- a/spec/services/ci/archive_trace_service_spec.rb
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -17,21 +17,12 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
context 'integration hooks' do
it do
- stub_feature_flags(datadog_integration_logs_collection: [job.project])
-
expect(job.project).to receive(:execute_integrations) do |data, hook_type|
expect(data).to eq Gitlab::DataBuilder::ArchiveTrace.build(job)
expect(hook_type).to eq :archive_trace_hooks
end
expect { subject }.not_to raise_error
end
-
- it 'with feature flag disabled' do
- stub_feature_flags(datadog_integration_logs_collection: false)
-
- expect(job.project).not_to receive(:execute_integrations)
- expect { subject }.not_to raise_error
- end
end
context 'when trace is already archived' do
diff --git a/spec/services/ci/build_erase_service_spec.rb b/spec/services/ci/build_erase_service_spec.rb
new file mode 100644
index 00000000000..e750a163621
--- /dev/null
+++ b/spec/services/ci/build_erase_service_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::BuildEraseService do
+ let_it_be(:user) { user }
+
+ let(:build) { create(:ci_build, :artifacts, :trace_artifact, artifacts_expire_at: 100.days.from_now) }
+
+ subject(:service) { described_class.new(build, user) }
+
+ describe '#execute' do
+ context 'when build is erasable' do
+ before do
+ allow(build).to receive(:erasable?).and_return(true)
+ end
+
+ it 'is successful' do
+ result = service.execute
+
+ expect(result).to be_success
+ end
+
+ it 'erases artifacts' do
+ service.execute
+
+ expect(build.artifacts_file).not_to be_present
+ expect(build.artifacts_metadata).not_to be_present
+ end
+
+ it 'erases trace' do
+ service.execute
+
+ expect(build.trace).not_to exist
+ end
+
+ it 'records erasure detail' do
+ freeze_time do
+ service.execute
+
+ expect(build.erased_by).to eq(user)
+ expect(build.erased_at).to eq(Time.current)
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'when project is undergoing statistics refresh' do
+ before do
+ allow(build.project).to receive(:refreshing_build_artifacts_size?).and_return(true)
+ end
+
+ it 'logs a warning' do
+ expect(Gitlab::ProjectStatsRefreshConflictsLogger)
+ .to receive(:warn_artifact_deletion_during_stats_refresh)
+ .with(method: 'Ci::BuildEraseService#execute', project_id: build.project_id)
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when build is not erasable' do
+ before do
+ allow(build).to receive(:erasable?).and_return(false)
+ end
+
+ it 'is not successful' do
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.http_status).to eq(:unprocessable_entity)
+ end
+
+ it 'does not erase artifacts' do
+ service.execute
+
+ expect(build.artifacts_file).to be_present
+ expect(build.artifacts_metadata).to be_present
+ end
+
+ it 'does not erase trace' do
+ service.execute
+
+ expect(build.trace).to exist
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/compare_reports_base_service_spec.rb b/spec/services/ci/compare_reports_base_service_spec.rb
index 9ce58c4972d..20d8cd37553 100644
--- a/spec/services/ci/compare_reports_base_service_spec.rb
+++ b/spec/services/ci/compare_reports_base_service_spec.rb
@@ -6,13 +6,13 @@ RSpec.describe Ci::CompareReportsBaseService do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
+ let!(:base_pipeline) { nil }
+ let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+ let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
+
describe '#latest?' do
subject { service.latest?(base_pipeline, head_pipeline, data) }
- let!(:base_pipeline) { nil }
- let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
- let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
-
context 'when cache key is latest' do
let(:data) { { key: key } }
@@ -35,4 +35,14 @@ RSpec.describe Ci::CompareReportsBaseService do
it { is_expected.to be_falsy }
end
end
+
+ describe '#execute' do
+ context 'when base_pipeline is running' do
+ let!(:base_pipeline) { create(:ci_pipeline, :running, project: project) }
+
+ subject { service.execute(base_pipeline, head_pipeline) }
+
+ it { is_expected.to eq(status: :parsing, key: key) }
+ end
+ end
end
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index 11fb564b843..9c02c5218f1 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -5,9 +5,12 @@ require 'spec_helper'
RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
include Ci::SourcePipelineHelpers
- let_it_be(:user) { create(:user) }
+ # Using let_it_be on user and projects for these specs can cause
+ # spec-ordering failures due to the project-based permissions
+ # associating them. They should be recreated every time.
+ let(:user) { create(:user) }
let(:upstream_project) { create(:project, :repository) }
- let_it_be(:downstream_project, refind: true) { create(:project, :repository) }
+ let(:downstream_project) { create(:project, :repository) }
let!(:upstream_pipeline) do
create(:ci_pipeline, :running, project: upstream_project)
@@ -440,10 +443,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
let!(:trigger_project_bridge) do
create(
- :ci_bridge, status: :pending,
- user: user,
- options: trigger_downstream_project,
- pipeline: child_pipeline
+ :ci_bridge, status: :pending, user: user, options: trigger_downstream_project, pipeline: child_pipeline
)
end
@@ -819,5 +819,60 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
end
end
+
+ context 'when a downstream pipeline has sibling pipelines' do
+ it_behaves_like 'logs downstream pipeline creation' do
+ let(:expected_root_pipeline) { upstream_pipeline }
+ let(:expected_downstream_relationship) { :multi_project }
+
+ # New downstream, plus upstream, plus two children of upstream created below
+ let(:expected_hierarchy_size) { 4 }
+
+ before do
+ create_list(:ci_pipeline, 2, child_of: upstream_pipeline)
+ end
+ end
+ end
+
+ context 'when the pipeline tree is too large' do
+ let_it_be(:parent) { create(:ci_pipeline) }
+ let_it_be(:child) { create(:ci_pipeline, child_of: parent) }
+ let_it_be(:sibling) { create(:ci_pipeline, child_of: parent) }
+
+ before do
+ stub_const("#{described_class}::MAX_HIERARCHY_SIZE", 3)
+ end
+
+ let(:bridge) do
+ create(:ci_bridge, status: :pending, user: user, options: trigger, pipeline: child)
+ end
+
+ it 'does not create a new pipeline' do
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ end
+
+ it 'drops the trigger job with an explanatory reason' do
+ subject
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('reached_max_pipeline_hierarchy_size')
+ end
+
+ context 'with :ci_limit_complete_hierarchy_size disabled' do
+ before do
+ stub_feature_flags(ci_limit_complete_hierarchy_size: false)
+ end
+
+ it 'creates a new pipeline' do
+ expect { subject }.to change { Ci::Pipeline.count }.by(1)
+ end
+
+ it 'marks the bridge job as successful' do
+ subject
+
+ expect(bridge.reload).to be_success
+ end
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/artifacts_spec.rb b/spec/services/ci/create_pipeline_service/artifacts_spec.rb
index 1ec30d68666..e5e405492a0 100644
--- a/spec/services/ci/create_pipeline_service/artifacts_spec.rb
+++ b/spec/services/ci/create_pipeline_service/artifacts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/cache_spec.rb b/spec/services/ci/create_pipeline_service/cache_spec.rb
index fe777bc50d9..82c3d374636 100644
--- a/spec/services/ci/create_pipeline_service/cache_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cache_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
context 'cache' do
let(:project) { create(:project, :custom_repo, files: files) }
let(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
index a920b90b97d..0ebcecdd6e6 100644
--- a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
+++ b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
describe 'creation errors and warnings' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
index e1d60ed57ef..74d3534eb45 100644
--- a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, '#execute' do
+RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_flag_corectness do
let_it_be(:group) { create(:group, name: 'my-organization') }
let(:upstream_project) { create(:project, :repository, name: 'upstream', group: group) }
diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
index a0cbf14d936..dafa227c4c8 100644
--- a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
+++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb b/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
index 716a929830e..3b042f05fc0 100644
--- a/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
+++ b/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
describe '!reference tags' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/dry_run_spec.rb b/spec/services/ci/create_pipeline_service/dry_run_spec.rb
index 9a7e97fb12b..de1ed251c82 100644
--- a/spec/services/ci/create_pipeline_service/dry_run_spec.rb
+++ b/spec/services/ci/create_pipeline_service/dry_run_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/environment_spec.rb b/spec/services/ci/create_pipeline_service/environment_spec.rb
index 43b5220334c..438cb6ac895 100644
--- a/spec/services/ci/create_pipeline_service/environment_spec.rb
+++ b/spec/services/ci/create_pipeline_service/environment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
@@ -45,5 +45,51 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
+
+ context 'when variables are dependent on stage name' do
+ let(:config) do
+ <<~YAML
+ deploy-review-app-1:
+ stage: deploy
+ environment: 'test/$CI_JOB_STAGE/1'
+ script:
+ - echo $SCOPED_VARIABLE
+ rules:
+ - if: $SCOPED_VARIABLE == 'my-value-1'
+
+ deploy-review-app-2:
+ stage: deploy
+ script:
+ - echo $SCOPED_VARIABLE
+ environment: 'test/$CI_JOB_STAGE/2'
+ rules:
+ - if: $SCOPED_VARIABLE == 'my-value-2'
+ YAML
+ end
+
+ before do
+ create(:ci_variable, key: 'SCOPED_VARIABLE', value: 'my-value-1', environment_scope: '*', project: project)
+ create(:ci_variable,
+ key: 'SCOPED_VARIABLE',
+ value: 'my-value-2',
+ environment_scope: 'test/deploy/*',
+ project: project
+ )
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates the pipeline successfully', :aggregate_failures do
+ pipeline = subject
+ build = pipeline.builds.first
+
+ expect(pipeline).to be_created_successfully
+ expect(Environment.find_by_name('test/deploy/2')).to be_persisted
+ expect(pipeline.builds.size).to eq(1)
+ expect(build.persisted_environment.name).to eq('test/deploy/2')
+ expect(build.name).to eq('deploy-review-app-2')
+ expect(build.environment).to eq('test/$CI_JOB_STAGE/2')
+ expect(build.variables.to_hash['SCOPED_VARIABLE']).to eq('my-value-2')
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb
index 7c698242921..e84726d31f6 100644
--- a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb
+++ b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:group) { create(:group, :private) }
let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group') }
let_it_be(:project) { create(:project, :repository, group: group) }
diff --git a/spec/services/ci/create_pipeline_service/include_spec.rb b/spec/services/ci/create_pipeline_service/include_spec.rb
index 849eb5885f6..67d8530525a 100644
--- a/spec/services/ci/create_pipeline_service/include_spec.rb
+++ b/spec/services/ci/create_pipeline_service/include_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
context 'include:' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/logger_spec.rb b/spec/services/ci/create_pipeline_service/logger_spec.rb
index 53e5f0dd7f2..2be23802757 100644
--- a/spec/services/ci/create_pipeline_service/logger_spec.rb
+++ b/spec/services/ci/create_pipeline_service/logger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
context 'pipeline logger' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -19,9 +19,9 @@ RSpec.describe Ci::CreatePipelineService do
let(:counters) do
{
'count' => a_kind_of(Numeric),
- 'avg' => a_kind_of(Numeric),
- 'max' => a_kind_of(Numeric),
- 'min' => a_kind_of(Numeric)
+ 'avg' => a_kind_of(Numeric),
+ 'max' => a_kind_of(Numeric),
+ 'min' => a_kind_of(Numeric)
}
end
diff --git a/spec/services/ci/create_pipeline_service/merge_requests_spec.rb b/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
index de19ef363fb..80f48451e5c 100644
--- a/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
+++ b/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
context 'merge requests handling' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/needs_spec.rb b/spec/services/ci/create_pipeline_service/needs_spec.rb
index abd17ccdd6a..38e330316ea 100644
--- a/spec/services/ci/create_pipeline_service/needs_spec.rb
+++ b/spec/services/ci/create_pipeline_service/needs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
context 'needs' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/parallel_spec.rb b/spec/services/ci/create_pipeline_service/parallel_spec.rb
index ae28b74fef5..5ee378a9719 100644
--- a/spec/services/ci/create_pipeline_service/parallel_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parallel_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
index f593707f460..cae88bb67cf 100644
--- a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
index 4326fa5533f..513cbbed6cd 100644
--- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, '#execute' do
+RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_flag_corectness do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -36,7 +36,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
expect(pipeline.statuses).to match_array [test, bridge]
expect(bridge.options).to eq(expected_bridge_options)
expect(bridge.yaml_variables)
- .to include(key: 'CROSS', value: 'downstream', public: true)
+ .to include(key: 'CROSS', value: 'downstream')
end
end
diff --git a/spec/services/ci/create_pipeline_service/partitioning_spec.rb b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
new file mode 100644
index 00000000000..43fbb74ede4
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.first_owner }
+
+ let(:service) { described_class.new(project, user, { ref: 'master' }) }
+ let(:config) do
+ <<-YAML
+ stages:
+ - build
+ - test
+ - deploy
+
+ build:
+ stage: build
+ script: make build
+
+ test:
+ stage: test
+ trigger:
+ include: child.yml
+
+ deploy:
+ stage: deploy
+ script: make deploy
+ environment: review/$CI_JOB_NAME
+ YAML
+ end
+
+ let(:pipeline) { service.execute(:push).payload }
+ let(:current_partition_id) { 123 }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ allow(Ci::Pipeline).to receive(:current_partition_value) { current_partition_id }
+ end
+
+ it 'assigns partition_id to pipeline' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.partition_id).to eq(current_partition_id)
+ end
+
+ it 'assigns partition_id to stages' do
+ stage_partition_ids = pipeline.stages.map(&:partition_id).uniq
+
+ expect(stage_partition_ids).to eq([current_partition_id])
+ end
+
+ it 'assigns partition_id to processables' do
+ processables_partition_ids = pipeline.processables.map(&:partition_id).uniq
+
+ expect(processables_partition_ids).to eq([current_partition_id])
+ end
+
+ it 'assigns partition_id to metadata' do
+ metadata_partition_ids = pipeline.processables.map { |job| job.metadata.partition_id }.uniq
+
+ expect(metadata_partition_ids).to eq([current_partition_id])
+ end
+
+ it 'correctly assigns partition and environment' do
+ metadata = find_metadata('deploy')
+
+ expect(metadata.partition_id).to eq(current_partition_id)
+ expect(metadata.expanded_environment_name).to eq('review/deploy')
+ end
+
+ context 'with pipeline variables' do
+ let(:variables_attributes) do
+ [
+ { key: 'SOME_VARIABLE', secret_value: 'SOME_VAL' },
+ { key: 'OTHER_VARIABLE', secret_value: 'OTHER_VAL' }
+ ]
+ end
+
+ let(:service) do
+ described_class.new(
+ project,
+ user,
+ { ref: 'master', variables_attributes: variables_attributes })
+ end
+
+ it 'assigns partition_id to pipeline' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.partition_id).to eq(current_partition_id)
+ end
+
+ it 'assigns partition_id to variables' do
+ variables_partition_ids = pipeline.variables.map(&:partition_id).uniq
+
+ expect(pipeline.variables.size).to eq(2)
+ expect(variables_partition_ids).to eq([current_partition_id])
+ end
+ end
+
+ context 'with parent child pipelines' do
+ before do
+ allow(Ci::Pipeline)
+ .to receive(:current_partition_value)
+ .and_return(current_partition_id, 301, 302)
+
+ allow_next_found_instance_of(Ci::Bridge) do |bridge|
+ allow(bridge).to receive(:yaml_for_downstream).and_return(child_config)
+ end
+ end
+
+ let(:config) do
+ <<-YAML
+ test:
+ trigger:
+ include: child.yml
+ YAML
+ end
+
+ let(:child_config) do
+ <<-YAML
+ test:
+ script: make test
+ YAML
+ end
+
+ it 'assigns partition values to child pipelines', :aggregate_failures, :sidekiq_inline do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.child_pipelines).to all be_created_successfully
+
+ child_partition_ids = pipeline.child_pipelines.map(&:partition_id).uniq
+ child_jobs = CommitStatus.where(commit_id: pipeline.child_pipelines)
+
+ expect(pipeline.partition_id).to eq(current_partition_id)
+ expect(child_partition_ids).to eq([current_partition_id])
+
+ expect(child_jobs).to all be_a(Ci::Build)
+ expect(child_jobs.pluck(:partition_id).uniq).to eq([current_partition_id])
+ end
+ end
+
+ def find_metadata(name)
+ pipeline
+ .processables
+ .find { |job| job.name == name }
+ .metadata
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
index c6e69862422..db110bdc608 100644
--- a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
+++ b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
describe '.pre/.post stages' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/rate_limit_spec.rb b/spec/services/ci/create_pipeline_service/rate_limit_spec.rb
index 0000296230f..dfa74870341 100644
--- a/spec/services/ci/create_pipeline_service/rate_limit_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rate_limit_spec.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :freeze_time, :clean_gitlab_redis_rate_limiting do
+RSpec.describe Ci::CreatePipelineService, :freeze_time,
+ :clean_gitlab_redis_rate_limiting,
+ :yaml_processor_feature_flag_corectness do
describe 'rate limiting' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 6e48141226d..fc57ca66d3a 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
diff --git a/spec/services/ci/create_pipeline_service/tags_spec.rb b/spec/services/ci/create_pipeline_service/tags_spec.rb
index 0774f9fff2a..7450df11eac 100644
--- a/spec/services/ci/create_pipeline_service/tags_spec.rb
+++ b/spec/services/ci/create_pipeline_service/tags_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
describe 'tags:' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -37,7 +37,7 @@ RSpec.describe Ci::CreatePipelineService do
context 'tags persistence' do
let(:config) do
{
- build: {
+ build: {
script: 'ls',
stage: 'build',
tags: build_tag_list(label: 'build')
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index a9442b0dc68..c2e80316d26 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :clean_gitlab_redis_cache do
include ProjectForksHelper
let_it_be_with_refind(:project) { create(:project, :repository) }
@@ -463,7 +463,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'pull it from Auto-DevOps' do
pipeline = execute_service.payload
expect(pipeline).to be_auto_devops_source
- expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality container_scanning eslint-sast secret_detection semgrep-sast test])
+ expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality container_scanning secret_detection semgrep-sast test])
end
end
diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb
index 7b3f67b192f..a2259f9813b 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -151,9 +151,8 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to match(
- a_hash_including(http_status: :bad_request,
- message: 'another artifact of the same type already exists',
- status: :error))
+ a_hash_including(
+ http_status: :bad_request, message: 'another artifact of the same type already exists', status: :error))
end
end
end
@@ -182,6 +181,18 @@ RSpec.describe Ci::JobArtifacts::CreateService do
end
end
+ context 'with job partitioning' do
+ let(:job) { create(:ci_build, project: project, partition_id: 123) }
+
+ it 'sets partition_id on artifacts' do
+ expect { subject }.to change { Ci::JobArtifact.count }
+
+ artifacts_partitions = job.job_artifacts.map(&:partition_id).uniq
+
+ expect(artifacts_partitions).to eq([123])
+ end
+ end
+
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|
diff --git a/spec/services/ci/job_artifacts/delete_service_spec.rb b/spec/services/ci/job_artifacts/delete_service_spec.rb
new file mode 100644
index 00000000000..62a755eb44a
--- /dev/null
+++ b/spec/services/ci/job_artifacts/delete_service_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobArtifacts::DeleteService do
+ let_it_be(:build, reload: true) do
+ create(:ci_build, :artifacts, :trace_artifact, artifacts_expire_at: 100.days.from_now)
+ end
+
+ subject(:service) { described_class.new(build) }
+
+ describe '#execute' do
+ it 'is successful' do
+ result = service.execute
+
+ expect(result).to be_success
+ end
+
+ it 'deletes erasable artifacts' do
+ expect { service.execute }.to change { build.job_artifacts.erasable.count }.from(2).to(0)
+ end
+
+ it 'does not delete trace' do
+ expect { service.execute }.not_to change { build.has_trace? }.from(true)
+ end
+
+ context 'when project is undergoing statistics refresh' do
+ before do
+ allow(build.project).to receive(:refreshing_build_artifacts_size?).and_return(true)
+ end
+
+ it 'logs a warning' do
+ expect(Gitlab::ProjectStatsRefreshConflictsLogger)
+ .to receive(:warn_artifact_deletion_during_stats_refresh)
+ .with(method: 'Ci::JobArtifacts::DeleteService#execute', project_id: build.project_id)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
index 9ca39d4d32e..54d1cacc068 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -221,6 +221,15 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
end
context 'with update_stats: false' do
+ let_it_be(:extra_artifact_with_file) do
+ create(:ci_job_artifact, :zip, project: artifact_with_file.project)
+ end
+
+ let(:artifacts) do
+ Ci::JobArtifact.where(id: [artifact_with_file.id, extra_artifact_with_file.id,
+ artifact_without_file.id, trace_artifact.id])
+ end
+
it 'does not update project statistics' do
expect(ProjectStatistics).not_to receive(:increment_statistic)
@@ -230,7 +239,7 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
it 'returns size statistics' do
expected_updates = {
statistics_updates: {
- artifact_with_file.project => -artifact_with_file.file.size,
+ artifact_with_file.project => -(artifact_with_file.file.size + extra_artifact_with_file.file.size),
artifact_without_file.project => 0
}
}
diff --git a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
new file mode 100644
index 00000000000..6d9fc4c8e34
--- /dev/null
+++ b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
+ describe '#execute', :clean_gitlab_redis_shared_state do
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
+ let(:start_time) { 1.week.ago }
+ let(:end_time) { 1.week.from_now }
+
+ subject(:track_artifact_report) { described_class.new.execute(pipeline) }
+
+ context 'when pipeline has test reports' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) }
+
+ before do
+ 2.times do
+ pipeline.builds << build(:ci_build, :test_reports, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
+ it 'tracks the event using HLLRedisCounter' do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user1.id)
+ .and_call_original
+
+ expect { track_artifact_report }
+ .to change {
+ counter.unique_events(event_names: test_event_name,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 1
+ end
+ end
+
+ context 'when pipeline does not have test reports' do
+ let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'does not track the event' do
+ track_artifact_report
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to receive(:track_event)
+ .with(anything, test_event_name)
+ end
+ end
+
+ context 'when a single user started multiple pipelines with test reports' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
+
+ it 'tracks all pipelines using HLLRedisCounter by one user_id' do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user1.id)
+ .and_call_original
+
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user1.id)
+ .and_call_original
+
+ expect do
+ described_class.new.execute(pipeline1)
+ described_class.new.execute(pipeline2)
+ end
+ .to change {
+ counter.unique_events(event_names: test_event_name,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 1
+ end
+ end
+
+ context 'when multiple users started multiple pipelines with test reports' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user2) }
+
+ it 'tracks all pipelines using HLLRedisCounter by multiple users' do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user1.id)
+ .and_call_original
+
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user1.id)
+ .and_call_original
+
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user2.id)
+ .and_call_original
+
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name, values: user2.id)
+ .and_call_original
+
+ expect do
+ described_class.new.execute(pipeline1)
+ described_class.new.execute(pipeline2)
+ end
+ .to change {
+ counter.unique_events(event_names: test_event_name,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 2
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/job_token_scope/add_project_service_spec.rb b/spec/services/ci/job_token_scope/add_project_service_spec.rb
index ba889465fac..bb6df4268dd 100644
--- a/spec/services/ci/job_token_scope/add_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/add_project_service_spec.rb
@@ -8,6 +8,14 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
let_it_be(:target_project) { create(:project) }
let_it_be(:current_user) { create(:user) }
+ shared_examples 'adds project' do |context|
+ it 'adds the project to the scope' do
+ expect do
+ expect(result).to be_success
+ end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
+ end
+ end
+
describe '#execute' do
subject(:result) { service.execute(target_project) }
@@ -18,10 +26,14 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
target_project.add_developer(current_user)
end
- it 'adds the project to the scope' do
- expect do
- expect(result).to be_success
- end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
+ it_behaves_like 'adds project'
+
+ context 'when token scope is disabled' do
+ before do
+ project.ci_cd_settings.update!(job_token_scope_enabled: false)
+ end
+
+ it_behaves_like 'adds project'
end
end
diff --git a/spec/services/ci/job_token_scope/remove_project_service_spec.rb b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
index 238fc879f54..155e60ac48e 100644
--- a/spec/services/ci/job_token_scope/remove_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
@@ -14,6 +14,14 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService do
target_project: target_project)
end
+ shared_examples 'removes project' do |context|
+ it 'removes the project from the scope' do
+ expect do
+ expect(result).to be_success
+ end.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1)
+ end
+ end
+
describe '#execute' do
subject(:result) { service.execute(target_project) }
@@ -24,10 +32,14 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService do
target_project.add_developer(current_user)
end
- it 'removes the project from the scope' do
- expect do
- expect(result).to be_success
- end.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1)
+ it_behaves_like 'removes project'
+
+ context 'when token scope is disabled' do
+ before do
+ project.ci_cd_settings.update!(job_token_scope_enabled: false)
+ end
+
+ it_behaves_like 'removes project'
end
end
diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index 1735f4cfc97..4953b18bfcc 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac
it 'returns variable list' do
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
- expect(subject['KEY3']).to eq({ value: 'val 3', description: nil })
- expect(subject['KEY4']).to eq({ value: 'val 4', description: nil })
+ expect(subject['KEY3']).to eq({ value: 'val 3' })
+ expect(subject['KEY4']).to eq({ value: 'val 4' })
end
end
diff --git a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
index 31548793bac..6d4dcf28108 100644
--- a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
+++ b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
@@ -51,6 +51,30 @@ RSpec.describe Ci::PipelineArtifacts::CoverageReportService do
let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
it_behaves_like 'creating or updating a pipeline coverage report'
+
+ context 'when ci_update_unlocked_pipeline_artifacts feature flag is enabled' do
+ it "artifact has pipeline's locked status" do
+ subject
+
+ artifact = Ci::PipelineArtifact.first
+
+ expect(artifact.locked).to eq(pipeline.locked)
+ end
+ end
+
+ context 'when ci_update_unlocked_pipeline_artifacts is disabled' do
+ before do
+ stub_feature_flags(ci_update_unlocked_pipeline_artifacts: false)
+ end
+
+ it 'artifact has unknown locked status' do
+ subject
+
+ artifact = Ci::PipelineArtifact.first
+
+ expect(artifact.locked).to eq('unknown')
+ end
+ end
end
context 'when pipeline has coverage report from child pipeline' do
diff --git a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
index 5568052e346..75233248113 100644
--- a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
+++ b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
@@ -51,6 +51,30 @@ RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService do
end
end
+ context 'when ci_update_unlocked_pipeline_artifacts feature flag is enabled' do
+ it "artifact has pipeline's locked status" do
+ subject
+
+ artifact = Ci::PipelineArtifact.first
+
+ expect(artifact.locked).to eq(head_pipeline.locked)
+ end
+ end
+
+ context 'when ci_update_unlocked_pipeline_artifacts is disabled' do
+ before do
+ stub_feature_flags(ci_update_unlocked_pipeline_artifacts: false)
+ end
+
+ it 'artifact has unknown locked status' do
+ subject
+
+ artifact = Ci::PipelineArtifact.first
+
+ expect(artifact.locked).to eq('unknown')
+ end
+ end
+
it 'does not persist the same artifact twice' do
2.times { described_class.new(head_pipeline).execute }
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
index 289e004fcce..7578afa7c50 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
@@ -6,11 +6,28 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
using RSpec::Parameterized::TableSyntax
let_it_be(:pipeline) { create(:ci_pipeline) }
- let_it_be(:build_a) { create(:ci_build, :success, name: 'build-a', stage: 'build', stage_idx: 0, pipeline: pipeline) }
- let_it_be(:build_b) { create(:ci_build, :failed, name: 'build-b', stage: 'build', stage_idx: 0, pipeline: pipeline) }
- let_it_be(:test_a) { create(:ci_build, :running, name: 'test-a', stage: 'test', stage_idx: 1, pipeline: pipeline) }
- let_it_be(:test_b) { create(:ci_build, :pending, name: 'test-b', stage: 'test', stage_idx: 1, pipeline: pipeline) }
- let_it_be(:deploy) { create(:ci_build, :created, name: 'deploy', stage: 'deploy', stage_idx: 2, pipeline: pipeline) }
+ let_it_be(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
+ let_it_be(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
+ let_it_be(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
+ let_it_be(:build_a) do
+ create(:ci_build, :success, name: 'build-a', ci_stage: build_stage, stage_idx: 0, pipeline: pipeline)
+ end
+
+ let_it_be(:build_b) do
+ create(:ci_build, :failed, name: 'build-b', ci_stage: build_stage, stage_idx: 0, pipeline: pipeline)
+ end
+
+ let_it_be(:test_a) do
+ create(:ci_build, :running, name: 'test-a', ci_stage: test_stage, stage_idx: 1, pipeline: pipeline)
+ end
+
+ let_it_be(:test_b) do
+ create(:ci_build, :pending, name: 'test-b', ci_stage: test_stage, stage_idx: 1, pipeline: pipeline)
+ end
+
+ let_it_be(:deploy) do
+ create(:ci_build, :created, name: 'deploy', ci_stage: deploy_stage, stage_idx: 2, pipeline: pipeline)
+ end
let(:collection) { described_class.new(pipeline) }
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 5bc508447c1..06bb6d39fe5 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -55,6 +55,8 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
statuses.each do |status|
if event == 'play'
status.play(user)
+ elsif event == 'retry'
+ ::Ci::RetryJobService.new(project, user).execute(status)
else
status.public_send("#{event}!")
end
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_1.yml b/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_1.yml
new file mode 100644
index 00000000000..b9b8eb2f532
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_1.yml
@@ -0,0 +1,55 @@
+config:
+ build:
+ script: exit $(($RANDOM % 2))
+
+ test:
+ script: exit 0
+ needs: [build]
+
+ deploy:
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ test: failed
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
+
+ - event: retry
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ test: pending
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+ - event: success
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ build: success
+ test: pending
+ deploy: created
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_2.yml b/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_2.yml
new file mode 100644
index 00000000000..c875ebab3c9
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_fail_and_retry_2.yml
@@ -0,0 +1,63 @@
+config:
+ build:
+ script: exit $(($RANDOM % 2))
+
+ test1:
+ script: exit 0
+ needs: [build]
+
+ test2:
+ script: exit 0
+ when: manual
+
+ deploy:
+ script: exit 0
+ needs: [test1, test2]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ build: pending
+ test1: created
+ test2: manual
+ deploy: skipped
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ test: failed
+ jobs:
+ build: failed
+ test1: skipped
+ test2: manual
+ deploy: skipped
+
+ - event: retry
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ test: pending
+ jobs:
+ build: pending
+ test1: created
+ test2: manual
+ deploy: skipped
+
+ - event: success
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ build: success
+ test1: pending
+ test2: manual
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_subsequent_manual_jobs.yml b/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_subsequent_manual_jobs.yml
new file mode 100644
index 00000000000..03ffda1caab
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_same_stage_with_subsequent_manual_jobs.yml
@@ -0,0 +1,65 @@
+config:
+ job1:
+ script: exit 0
+ when: manual
+
+ job2:
+ script: exit 0
+ needs: [job1]
+
+ job3:
+ script: exit 0
+ when: manual
+ needs: [job2]
+
+ job4:
+ script: exit 0
+ needs: [job3]
+
+init:
+ expect:
+ pipeline: skipped
+ stages:
+ test: skipped
+ jobs:
+ job1: manual
+ job2: skipped
+ job3: skipped
+ job4: skipped
+
+transitions:
+ - event: play
+ jobs: [job1]
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ job1: pending
+ job2: created
+ job3: created
+ job4: created
+
+ - event: success
+ jobs: [job1]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ job1: success
+ job2: pending
+ job3: created
+ job4: created
+
+ - event: success
+ jobs: [job2]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ job1: success
+ job2: success
+ job3: manual
+ job4: skipped
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
index b8e4fb19f5d..2f094583f1a 100644
--- a/spec/services/ci/pipeline_schedule_service_spec.rb
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -3,14 +3,15 @@
require 'spec_helper'
RSpec.describe Ci::PipelineScheduleService do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
let(:service) { described_class.new(project, user) }
describe '#execute' do
subject { service.execute(schedule) }
- let(:schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+ let_it_be(:schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
it 'schedules next run' do
expect(schedule).to receive(:schedule_next_run!)
@@ -34,9 +35,7 @@ RSpec.describe Ci::PipelineScheduleService do
end
context 'when the project is missing' do
- before do
- project.delete
- end
+ let(:project) { create(:project).tap(&:delete) }
it 'does not raise an exception' do
expect { subject }.not_to raise_error
diff --git a/spec/services/ci/pipelines/add_job_service_spec.rb b/spec/services/ci/pipelines/add_job_service_spec.rb
index 560724a1c6a..e735b2752d9 100644
--- a/spec/services/ci/pipelines/add_job_service_spec.rb
+++ b/spec/services/ci/pipelines/add_job_service_spec.rb
@@ -34,6 +34,14 @@ RSpec.describe Ci::Pipelines::AddJobService do
).and change { job.metadata.project }.to(pipeline.project)
end
+ it 'assigns partition_id to job and metadata' do
+ pipeline.partition_id = 123
+
+ expect { execute }
+ .to change(job, :partition_id).to(pipeline.partition_id)
+ .and change { job.metadata.partition_id }.to(pipeline.partition_id)
+ end
+
it 'returns a service response with the job as payload' do
expect(execute).to be_success
expect(execute.payload[:job]).to eq(job)
diff --git a/spec/services/ci/pipelines/hook_service_spec.rb b/spec/services/ci/pipelines/hook_service_spec.rb
index 0e1ef6afd0d..8d138a3d957 100644
--- a/spec/services/ci/pipelines/hook_service_spec.rb
+++ b/spec/services/ci/pipelines/hook_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Ci::Pipelines::HookService do
describe '#execute_hooks' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
- let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }
+ let_it_be(:pipeline, reload: true) { create(:ci_empty_pipeline, :created, project: project) }
let(:hook_enabled) { true }
let!(:hook) { create(:project_hook, project: project, pipeline_events: hook_enabled) }
diff --git a/spec/services/ci/play_manual_stage_service_spec.rb b/spec/services/ci/play_manual_stage_service_spec.rb
index b3ae92aa787..24f0a21f3dd 100644
--- a/spec/services/ci/play_manual_stage_service_spec.rb
+++ b/spec/services/ci/play_manual_stage_service_spec.rb
@@ -75,7 +75,6 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
options.merge!({
when: 'manual',
pipeline: pipeline,
- stage: stage.name,
stage_id: stage.id,
user: pipeline.user
})
@@ -87,7 +86,6 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
options.merge!({
when: 'manual',
pipeline: pipeline,
- stage: stage.name,
stage_id: stage.id,
user: pipeline.user,
downstream: downstream_project
diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb
index 241ac4995ff..7ab7911e578 100644
--- a/spec/services/ci/process_sync_events_service_spec.rb
+++ b/spec/services/ci/process_sync_events_service_spec.rb
@@ -120,13 +120,15 @@ RSpec.describe Ci::ProcessSyncEventsService do
before do
Namespaces::SyncEvent.delete_all
+ # Creates a sync event for group, and the ProjectNamespace of project1 & project2: 3 in total
group.update!(parent: parent_group_2)
+ # Creates a sync event for parent_group2 and all the children: 4 in total
parent_group_2.update!(parent: parent_group_1)
end
shared_examples 'event consuming' do
it 'consumes events' do
- expect { execute }.to change(Namespaces::SyncEvent, :count).from(2).to(0)
+ expect { execute }.to change(Namespaces::SyncEvent, :count).from(7).to(0)
expect(group.reload.ci_namespace_mirror).to have_attributes(
traversal_ids: [parent_group_1.id, parent_group_2.id, group.id]
@@ -134,6 +136,12 @@ RSpec.describe Ci::ProcessSyncEventsService do
expect(parent_group_2.reload.ci_namespace_mirror).to have_attributes(
traversal_ids: [parent_group_1.id, parent_group_2.id]
)
+ expect(project1.reload.project_namespace).to have_attributes(
+ traversal_ids: [parent_group_1.id, parent_group_2.id, group.id, project1.project_namespace.id]
+ )
+ expect(project2.reload.project_namespace).to have_attributes(
+ traversal_ids: [parent_group_1.id, parent_group_2.id, group.id, project2.project_namespace.id]
+ )
end
end
@@ -157,7 +165,7 @@ RSpec.describe Ci::ProcessSyncEventsService do
end
it 'does not enqueue Namespaces::ProcessSyncEventsWorker if no left' do
- stub_const("#{described_class}::BATCH_SIZE", 2)
+ stub_const("#{described_class}::BATCH_SIZE", 7)
expect(Namespaces::ProcessSyncEventsWorker).not_to receive(:perform_async)
diff --git a/spec/services/ci/queue/pending_builds_strategy_spec.rb b/spec/services/ci/queue/pending_builds_strategy_spec.rb
new file mode 100644
index 00000000000..6f22c256c17
--- /dev/null
+++ b/spec/services/ci/queue/pending_builds_strategy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Queue::PendingBuildsStrategy do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let!(:build_1) { create(:ci_build, :created, pipeline: pipeline) }
+ let!(:build_2) { create(:ci_build, :created, pipeline: pipeline) }
+ let!(:build_3) { create(:ci_build, :created, pipeline: pipeline) }
+ let!(:pending_build_1) { create(:ci_pending_build, build: build_2, project: project) }
+ let!(:pending_build_2) { create(:ci_pending_build, build: build_3, project: project) }
+ let!(:pending_build_3) { create(:ci_pending_build, build: build_1, project: project) }
+
+ describe 'builds_for_group_runner' do
+ it 'returns builds ordered by build ID' do
+ strategy = described_class.new(group_runner)
+ expect(strategy.builds_for_group_runner).to eq([pending_build_3, pending_build_1, pending_build_2])
+ end
+ end
+end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index cabd60a22d1..e2e760b9812 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -571,10 +571,6 @@ module Ci
context 'when artifacts of depended job has been erased' do
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
- before do
- pre_stage_job.erase
- end
-
it_behaves_like 'not pick'
end
@@ -612,10 +608,6 @@ module Ci
context 'when artifacts of depended job has been erased' do
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
- before do
- pre_stage_job.erase
- end
-
it { expect(subject).to eq(pending_job) }
end
end
diff --git a/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb b/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb
index 194203a422c..3d1abe290bc 100644
--- a/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb
+++ b/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupService do
+ include ConcurrentHelpers
+
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@@ -134,6 +136,19 @@ RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupService do
end
end
end
+
+ context 'when parallel services are running' do
+ it 'can run the same command in parallel' do
+ parallel_num = 4
+
+ blocks = Array.new(parallel_num).map do
+ -> { subject }
+ end
+
+ run_parallel(blocks)
+ expect(build.reload).to be_pending
+ end
+ end
end
context 'when there are no available resources' do
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index b14e4187c7a..69f19c5acf2 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -7,14 +7,13 @@ RSpec.describe Ci::RetryJobService do
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
+ create(:ci_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
end
let_it_be(:stage) do
create(:ci_stage, project: project,
- pipeline: pipeline,
- name: 'test')
+ pipeline: pipeline,
+ name: 'test')
end
let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
@@ -31,9 +30,8 @@ RSpec.describe Ci::RetryJobService do
let_it_be(:downstream_project) { create(:project, :repository) }
let_it_be_with_refind(:job) do
- create(
- :ci_bridge, :success, pipeline: pipeline, downstream: downstream_project,
- description: 'a trigger job', stage_id: stage.id
+ create(:ci_bridge, :success,
+ pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage
)
end
@@ -45,13 +43,13 @@ RSpec.describe Ci::RetryJobService do
end
shared_context 'retryable build' do
- let_it_be_with_refind(:job) { create(:ci_build, :success, pipeline: pipeline, stage_id: stage.id) }
+ let_it_be_with_refind(:job) { create(:ci_build, :success, pipeline: pipeline, ci_stage: stage) }
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:job_to_clone) do
create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
:allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
- description: 'my-job', stage: 'test', stage_id: stage.id,
+ description: 'my-job', ci_stage: stage,
pipeline: pipeline, auto_canceled_by: another_pipeline,
scheduled_at: 10.seconds.since)
end
@@ -66,8 +64,7 @@ RSpec.describe Ci::RetryJobService do
let(:job) { job_to_clone }
before_all do
- # Make sure that job has both `stage_id` and `stage`
- job_to_clone.update!(stage: 'test', stage_id: stage.id)
+ job_to_clone.update!(ci_stage: stage)
create(:ci_build_need, build: job_to_clone)
end
@@ -126,16 +123,16 @@ RSpec.describe Ci::RetryJobService do
end
context 'when there are subsequent processables that are skipped' do
+ let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
+
let!(:subsequent_build) do
create(:ci_build, :skipped, stage_idx: 2,
pipeline: pipeline,
- stage: 'deploy')
+ ci_stage: stage)
end
let!(:subsequent_bridge) do
- create(:ci_bridge, :skipped, stage_idx: 2,
- pipeline: pipeline,
- stage: 'deploy')
+ create(:ci_bridge, :skipped, stage_idx: 2, pipeline: pipeline, ci_stage: stage)
end
it 'resumes pipeline processing in the subsequent stage' do
@@ -156,8 +153,8 @@ RSpec.describe Ci::RetryJobService do
context 'when the pipeline has other jobs' do
let!(:stage2) { create(:ci_stage, project: project, pipeline: pipeline, name: 'deploy') }
- let!(:build2) { create(:ci_build, pipeline: pipeline, stage_id: stage.id ) }
- let!(:deploy) { create(:ci_build, pipeline: pipeline, stage_id: stage2.id) }
+ let!(:build2) { create(:ci_build, pipeline: pipeline, ci_stage: stage ) }
+ let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: stage2) }
let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: build2.name) }
context 'when job has a nil scheduling_type' do
@@ -227,7 +224,7 @@ RSpec.describe Ci::RetryJobService do
context 'when a build with a deployment is retried' do
let!(:job) do
create(:ci_build, :with_deployment, :deploy_to_production,
- pipeline: pipeline, stage_id: stage.id, project: project)
+ pipeline: pipeline, ci_stage: stage, project: project)
end
it 'creates a new deployment' do
@@ -245,10 +242,13 @@ RSpec.describe Ci::RetryJobService do
let(:environment_name) { 'review/$CI_COMMIT_REF_SLUG-$GITLAB_USER_ID' }
let!(:job) do
- create(:ci_build, :with_deployment, environment: environment_name,
- options: { environment: { name: environment_name } },
- pipeline: pipeline, stage_id: stage.id, project: project,
- user: other_developer)
+ create(:ci_build, :with_deployment,
+ environment: environment_name,
+ options: { environment: { name: environment_name } },
+ pipeline: pipeline,
+ ci_stage: stage,
+ project: project,
+ user: other_developer)
end
it 'creates a new deployment' do
@@ -307,22 +307,24 @@ RSpec.describe Ci::RetryJobService do
it_behaves_like 'retries the job'
context 'when there are subsequent jobs that are skipped' do
+ let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
+
let!(:subsequent_build) do
create(:ci_build, :skipped, stage_idx: 2,
pipeline: pipeline,
- stage: 'deploy')
+ stage_id: stage.id)
end
let!(:subsequent_bridge) do
create(:ci_bridge, :skipped, stage_idx: 2,
pipeline: pipeline,
- stage: 'deploy')
+ stage_id: stage.id)
end
it 'does not cause an N+1 when updating the job ownership' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { service.execute(job) }.count
- create_list(:ci_build, 2, :skipped, stage_idx: job.stage_idx + 1, pipeline: pipeline, stage: 'deploy')
+ create_list(:ci_build, 2, :skipped, stage_idx: job.stage_idx + 1, pipeline: pipeline, stage_id: stage.id)
expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 24272801480..0a1e767539d 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -9,6 +9,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:service) { described_class.new(project, user) }
+ let(:build_stage) { create(:ci_stage, name: 'build', position: 0, pipeline: pipeline) }
+ let(:test_stage) { create(:ci_stage, name: 'test', position: 1, pipeline: pipeline) }
+ let(:deploy_stage) { create(:ci_stage, name: 'deploy', position: 2, pipeline: pipeline) }
context 'when user has full ability to modify pipeline' do
before do
@@ -20,8 +23,8 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there are already retried jobs present' do
before do
- create_build('rspec', :canceled, 0, retried: true)
- create_build('rspec', :failed, 0)
+ create_build('rspec', :canceled, build_stage, retried: true)
+ create_build('rspec', :failed, build_stage)
end
it 'does not retry jobs that has already been retried' do
@@ -33,9 +36,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there are failed builds in the last stage' do
before do
- create_build('rspec 1', :success, 0)
- create_build('rspec 2', :failed, 1)
- create_build('rspec 3', :canceled, 1)
+ create_build('rspec 1', :success, build_stage)
+ create_build('rspec 2', :failed, test_stage)
+ create_build('rspec 3', :canceled, test_stage)
end
it 'enqueues all builds in the last stage' do
@@ -49,10 +52,10 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there are failed or canceled builds in the first stage' do
before do
- create_build('rspec 1', :failed, 0)
- create_build('rspec 2', :canceled, 0)
- create_build('rspec 3', :canceled, 1)
- create_build('spinach 1', :canceled, 2)
+ create_build('rspec 1', :failed, build_stage)
+ create_build('rspec 2', :canceled, build_stage)
+ create_build('rspec 3', :canceled, test_stage)
+ create_build('spinach 1', :canceled, deploy_stage)
end
it 'retries builds failed builds and marks subsequent for processing' do
@@ -80,10 +83,10 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is failed build present which was run on failure' do
before do
- create_build('rspec 1', :failed, 0)
- create_build('rspec 2', :canceled, 0)
- create_build('rspec 3', :canceled, 1)
- create_build('report 1', :failed, 2)
+ create_build('rspec 1', :failed, build_stage)
+ create_build('rspec 2', :canceled, build_stage)
+ create_build('rspec 3', :canceled, test_stage)
+ create_build('report 1', :failed, deploy_stage)
end
it 'retries builds only in the first stage' do
@@ -105,9 +108,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a failed test in a DAG' do
before do
- create_build('build', :success, 0)
- create_build('build2', :success, 0)
- test_build = create_build('test', :failed, 1, scheduling_type: :dag)
+ create_build('build', :success, build_stage)
+ create_build('build2', :success, build_stage)
+ test_build = create_build('test', :failed, test_stage, scheduling_type: :dag)
create(:ci_build_need, build: test_build, name: 'build')
create(:ci_build_need, build: test_build, name: 'build2')
end
@@ -123,7 +126,7 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a failed DAG test without needs' do
before do
- create_build('deploy', :failed, 2, scheduling_type: :dag)
+ create_build('deploy', :failed, deploy_stage, scheduling_type: :dag)
end
it 'retries the test' do
@@ -139,10 +142,10 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when the last stage was skipped' do
before do
- create_build('build 1', :success, 0)
- create_build('test 2', :failed, 1)
- create_build('report 3', :skipped, 2)
- create_build('report 4', :skipped, 2)
+ create_build('build 1', :success, build_stage)
+ create_build('test 2', :failed, test_stage)
+ create_build('report 3', :skipped, deploy_stage)
+ create_build('report 4', :skipped, deploy_stage)
end
it 'retries builds only in the first stage' do
@@ -160,9 +163,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there are optional manual actions only' do
context 'when there is a canceled manual action in first stage' do
before do
- create_build('rspec 1', :failed, 0)
- create_build('staging', :canceled, 0, when: :manual, allow_failure: true)
- create_build('rspec 2', :canceled, 1)
+ create_build('rspec 1', :failed, build_stage)
+ create_build('staging', :canceled, build_stage, when: :manual, allow_failure: true)
+ create_build('rspec 2', :canceled, test_stage)
end
it 'retries failed builds and marks subsequent for processing' do
@@ -189,9 +192,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when pipeline has blocking manual actions defined' do
context 'when pipeline retry should enqueue builds' do
before do
- create_build('test', :failed, 0)
- create_build('deploy', :canceled, 0, when: :manual, allow_failure: false)
- create_build('verify', :canceled, 1)
+ create_build('test', :failed, build_stage)
+ create_build('deploy', :canceled, build_stage, when: :manual, allow_failure: false)
+ create_build('verify', :canceled, test_stage)
end
it 'retries failed builds' do
@@ -206,10 +209,10 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when pipeline retry should block pipeline immediately' do
before do
- create_build('test', :success, 0)
- create_build('deploy:1', :success, 1, when: :manual, allow_failure: false)
- create_build('deploy:2', :failed, 1, when: :manual, allow_failure: false)
- create_build('verify', :canceled, 2)
+ create_build('test', :success, build_stage)
+ create_build('deploy:1', :success, test_stage, when: :manual, allow_failure: false)
+ create_build('deploy:2', :failed, test_stage, when: :manual, allow_failure: false)
+ create_build('verify', :canceled, deploy_stage)
end
it 'reprocesses blocking manual action and blocks pipeline' do
@@ -225,9 +228,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a skipped manual action in last stage' do
before do
- create_build('rspec 1', :canceled, 0)
- create_build('rspec 2', :skipped, 0, when: :manual, allow_failure: true)
- create_build('staging', :skipped, 1, when: :manual, allow_failure: true)
+ create_build('rspec 1', :canceled, build_stage)
+ create_build('rspec 2', :skipped, build_stage, when: :manual, allow_failure: true)
+ create_build('staging', :skipped, test_stage, when: :manual, allow_failure: true)
end
it 'retries canceled job and reprocesses manual actions' do
@@ -242,8 +245,8 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a created manual action in the last stage' do
before do
- create_build('rspec 1', :canceled, 0)
- create_build('staging', :created, 1, when: :manual, allow_failure: true)
+ create_build('rspec 1', :canceled, build_stage)
+ create_build('staging', :created, test_stage, when: :manual, allow_failure: true)
end
it 'retries canceled job and does not update the manual action' do
@@ -257,8 +260,8 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a created manual action in the first stage' do
before do
- create_build('rspec 1', :canceled, 0)
- create_build('staging', :created, 0, when: :manual, allow_failure: true)
+ create_build('rspec 1', :canceled, build_stage)
+ create_build('staging', :created, build_stage, when: :manual, allow_failure: true)
end
it 'retries canceled job and processes the manual action' do
@@ -285,9 +288,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
end
context 'when pipeline has processables with nil scheduling_type' do
- let!(:build1) { create_build('build1', :success, 0) }
- let!(:build2) { create_build('build2', :failed, 0) }
- let!(:build3) { create_build('build3', :failed, 1) }
+ let!(:build1) { create_build('build1', :success, build_stage) }
+ let!(:build2) { create_build('build2', :failed, build_stage) }
+ let!(:build3) { create_build('build3', :failed, test_stage) }
let!(:build3_needs_build1) { create(:ci_build_need, build: build3, name: build1.name) }
before do
@@ -319,10 +322,10 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there are skipped jobs in later stages' do
before do
- create_build('build 1', :success, 0)
- create_build('test 2', :failed, 1)
- create_build('report 3', :skipped, 2)
- create_bridge('deploy 4', :skipped, 2)
+ create_build('build 1', :success, build_stage)
+ create_build('test 2', :failed, test_stage)
+ create_build('report 3', :skipped, deploy_stage)
+ create_bridge('deploy 4', :skipped, deploy_stage)
end
it 'retries failed jobs and processes skipped jobs' do
@@ -374,9 +377,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a failed manual action present' do
before do
- create_build('test', :failed, 0)
- create_build('deploy', :failed, 0, when: :manual)
- create_build('verify', :canceled, 1)
+ create_build('test', :failed, build_stage)
+ create_build('deploy', :failed, build_stage, when: :manual)
+ create_build('verify', :canceled, test_stage)
end
it 'returns an error' do
@@ -390,9 +393,9 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
context 'when there is a failed manual action in later stage' do
before do
- create_build('test', :failed, 0)
- create_build('deploy', :failed, 1, when: :manual)
- create_build('verify', :canceled, 2)
+ create_build('test', :failed, build_stage)
+ create_build('deploy', :failed, test_stage, when: :manual)
+ create_build('verify', :canceled, deploy_stage)
end
it 'returns an error' do
@@ -418,7 +421,7 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
target_project: project,
source_branch: 'fixes',
allow_collaboration: true)
- create_build('rspec 1', :failed, 1)
+ create_build('rspec 1', :failed, test_stage)
end
it 'allows to retry failed pipeline' do
@@ -441,19 +444,19 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
statuses.latest.find_by(name: name)
end
- def create_build(name, status, stage_num, **opts)
- create_processable(:ci_build, name, status, stage_num, **opts)
+ def create_build(name, status, stage, **opts)
+ create_processable(:ci_build, name, status, stage, **opts)
end
- def create_bridge(name, status, stage_num, **opts)
- create_processable(:ci_bridge, name, status, stage_num, **opts)
+ def create_bridge(name, status, stage, **opts)
+ create_processable(:ci_bridge, name, status, stage, **opts)
end
- def create_processable(type, name, status, stage_num, **opts)
+ def create_processable(type, name, status, stage, **opts)
create(type, name: name,
status: status,
- stage: "stage_#{stage_num}",
- stage_idx: stage_num,
+ ci_stage: stage,
+ stage_idx: stage.position,
pipeline: pipeline, **opts) do |_job|
::Ci::ProcessPipelineService.new(pipeline).execute
end
diff --git a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
new file mode 100644
index 00000000000..0d2e237c87b
--- /dev/null
+++ b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute' do
+ subject(:execute) { described_class.new(runner: runner, current_user: user, project_ids: project_ids).execute }
+
+ let_it_be(:owner_project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:original_projects) { [owner_project, project2] }
+
+ let(:runner) { create(:ci_runner, :project, projects: original_projects) }
+
+ context 'without user' do
+ let(:user) { nil }
+ let(:project_ids) { [project2.id] }
+
+ it 'does not call assign_to on runner and returns error response', :aggregate_failures do
+ expect(runner).not_to receive(:assign_to)
+
+ expect(execute).to be_error
+ expect(execute.message).to eq('user not allowed to assign runner')
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:user) { build(:user) }
+ let(:project_ids) { [project2.id] }
+
+ it 'does not call assign_to on runner and returns error message' do
+ expect(runner).not_to receive(:assign_to)
+
+ expect(execute).to be_error
+ expect(execute.message).to eq('user not allowed to assign runner')
+ end
+ end
+
+ context 'with admin user', :enable_admin_mode do
+ let_it_be(:user) { create(:user, :admin) }
+
+ let(:project3) { create(:project) }
+ let(:project4) { create(:project) }
+
+ context 'with successful requests' do
+ context 'when disassociating a project' do
+ let(:project_ids) { [project3.id, project4.id] }
+
+ it 'reassigns associated projects and returns success response' do
+ expect(execute).to be_success
+ expect(runner.reload.projects.ids).to eq([owner_project.id] + project_ids)
+ end
+ end
+
+ context 'when disassociating no projects' do
+ let(:project_ids) { [project2.id, project3.id] }
+
+ it 'reassigns associated projects and returns success response' do
+ expect(execute).to be_success
+ expect(runner.reload.projects.ids).to eq([owner_project.id] + project_ids)
+ end
+ end
+ end
+
+ context 'with failing assign_to requests' do
+ let(:project_ids) { [project3.id, project4.id] }
+
+ it 'returns error response and rolls back transaction' do
+ expect(runner).to receive(:assign_to).with(project4, user).once.and_return(false)
+
+ expect(execute).to be_error
+ expect(runner.reload.projects).to eq(original_projects)
+ end
+ end
+
+ context 'with failing destroy calls' do
+ let(:project_ids) { [project3.id, project4.id] }
+
+ it 'returns error response and rolls back transaction' do
+ allow_next_found_instance_of(Ci::RunnerProject) do |runner_project|
+ allow(runner_project).to receive(:destroy).and_return(false)
+ end
+
+ expect(execute).to be_error
+ expect(runner.reload.projects).to eq(original_projects)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/runners/update_runner_service_spec.rb b/spec/services/ci/runners/update_runner_service_spec.rb
index e008fde9982..1f953ac4cbb 100644
--- a/spec/services/ci/runners/update_runner_service_spec.rb
+++ b/spec/services/ci/runners/update_runner_service_spec.rb
@@ -2,69 +2,65 @@
require 'spec_helper'
-RSpec.describe Ci::Runners::UpdateRunnerService do
+RSpec.describe Ci::Runners::UpdateRunnerService, '#execute' do
+ subject(:execute) { described_class.new(runner).execute(params) }
+
let(:runner) { create(:ci_runner) }
- describe '#update' do
- before do
- allow(runner).to receive(:tick_runner_queue)
- end
+ before do
+ allow(runner).to receive(:tick_runner_queue)
+ end
- context 'with description params' do
- let(:params) { { description: 'new runner' } }
+ context 'with description params' do
+ let(:params) { { description: 'new runner' } }
- it 'updates the runner and ticking the queue' do
- expect(update).to be_truthy
+ it 'updates the runner and ticking the queue' do
+ expect(execute).to be_success
- runner.reload
+ runner.reload
- expect(runner).to have_received(:tick_runner_queue)
- expect(runner.description).to eq('new runner')
- end
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.description).to eq('new runner')
end
+ end
- context 'with paused param' do
- let(:params) { { paused: true } }
+ context 'with paused param' do
+ let(:params) { { paused: true } }
- it 'updates the runner and ticking the queue' do
- expect(runner.active).to be_truthy
- expect(update).to be_truthy
+ it 'updates the runner and ticking the queue' do
+ expect(runner.active).to be_truthy
+ expect(execute).to be_success
- runner.reload
+ runner.reload
- expect(runner).to have_received(:tick_runner_queue)
- expect(runner.active).to be_falsey
- end
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.active).to be_falsey
end
+ end
- context 'with cost factor params' do
- let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } }
+ context 'with cost factor params' do
+ let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } }
- it 'updates the runner cost factors' do
- expect(update).to be_truthy
+ it 'updates the runner cost factors' do
+ expect(execute).to be_success
- runner.reload
+ runner.reload
- expect(runner.public_projects_minutes_cost_factor).to eq(1.1)
- expect(runner.private_projects_minutes_cost_factor).to eq(2.2)
- end
+ expect(runner.public_projects_minutes_cost_factor).to eq(1.1)
+ expect(runner.private_projects_minutes_cost_factor).to eq(2.2)
end
+ end
- context 'when params are not valid' do
- let(:params) { { run_untagged: false } }
-
- it 'does not update and give false because it is not valid' do
- expect(update).to be_falsey
+ context 'when params are not valid' do
+ let(:params) { { run_untagged: false } }
- runner.reload
+ it 'does not update and returns error because it is not valid' do
+ expect(execute).to be_error
- expect(runner).not_to have_received(:tick_runner_queue)
- expect(runner.run_untagged).to be_truthy
- end
- end
+ runner.reload
- def update
- described_class.new(runner).update(params) # rubocop: disable Rails/SaveBang
+ expect(runner).not_to have_received(:tick_runner_queue)
+ expect(runner.run_untagged).to be_truthy
end
end
end
diff --git a/spec/services/ci/unlock_artifacts_service_spec.rb b/spec/services/ci/unlock_artifacts_service_spec.rb
index 94d39fc9f14..776019f03f8 100644
--- a/spec/services/ci/unlock_artifacts_service_spec.rb
+++ b/spec/services/ci/unlock_artifacts_service_spec.rb
@@ -5,11 +5,15 @@ require 'spec_helper'
RSpec.describe Ci::UnlockArtifactsService do
using RSpec::Parameterized::TableSyntax
- where(:tag, :ci_update_unlocked_job_artifacts) do
- false | false
- false | true
- true | false
- true | true
+ where(:tag, :ci_update_unlocked_job_artifacts, :ci_update_unlocked_pipeline_artifacts) do
+ false | false | false
+ false | true | false
+ true | false | false
+ true | true | false
+ false | false | true
+ false | true | true
+ true | false | true
+ true | true | true
end
with_them do
@@ -22,6 +26,7 @@ RSpec.describe Ci::UnlockArtifactsService do
let!(:old_unlocked_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :unlocked) }
let!(:older_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
let!(:older_ambiguous_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: !tag, project: project, locked: :artifacts_locked) }
+ let!(:code_coverage_pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
let!(:pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
let!(:child_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
let!(:newer_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
@@ -30,7 +35,8 @@ RSpec.describe Ci::UnlockArtifactsService do
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
- stub_feature_flags(ci_update_unlocked_job_artifacts: ci_update_unlocked_job_artifacts)
+ stub_feature_flags(ci_update_unlocked_job_artifacts: ci_update_unlocked_job_artifacts,
+ ci_update_unlocked_pipeline_artifacts: ci_update_unlocked_pipeline_artifacts)
end
describe '#execute' do
@@ -72,6 +78,14 @@ RSpec.describe Ci::UnlockArtifactsService do
expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(2)
end
+
+ it 'unlocks pipeline artifact records' do
+ if ci_update_unlocked_job_artifacts && ci_update_unlocked_pipeline_artifacts
+ expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
+ else
+ expect { execute }.not_to change { ::Ci::PipelineArtifact.artifact_unlocked.count }
+ end
+ end
end
context 'when running on just the ref' do
@@ -106,6 +120,14 @@ RSpec.describe Ci::UnlockArtifactsService do
expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(8)
end
+
+ it 'unlocks pipeline artifact records' do
+ if ci_update_unlocked_job_artifacts && ci_update_unlocked_pipeline_artifacts
+ expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
+ else
+ expect { execute }.not_to change { ::Ci::PipelineArtifact.artifact_unlocked.count }
+ end
+ end
end
end
diff --git a/spec/services/container_expiration_policies/cleanup_service_spec.rb b/spec/services/container_expiration_policies/cleanup_service_spec.rb
index c265ce74d14..6e1be7271e1 100644
--- a/spec/services/container_expiration_policies/cleanup_service_spec.rb
+++ b/spec/services/container_expiration_policies/cleanup_service_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
it 'completely clean up the repository' do
expect(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:new).with(repository, nil, cleanup_tags_service_params).and_return(cleanup_tags_service)
+ .to receive(:new).with(container_repository: repository, params: cleanup_tags_service_params).and_return(cleanup_tags_service)
expect(cleanup_tags_service).to receive(:execute).and_return(status: :success, deleted_size: 1)
response = subject
diff --git a/spec/services/deployments/link_merge_requests_service_spec.rb b/spec/services/deployments/link_merge_requests_service_spec.rb
index 62adc834733..a653cd2b48b 100644
--- a/spec/services/deployments/link_merge_requests_service_spec.rb
+++ b/spec/services/deployments/link_merge_requests_service_spec.rb
@@ -159,10 +159,10 @@ RSpec.describe Deployments::LinkMergeRequestsService do
it "doesn't link the same merge_request twice" do
create(:merge_request, :merged, merge_commit_sha: mr1_merge_commit_sha,
- source_project: project)
+ source_project: project)
picked_mr = create(:merge_request, :merged, merge_commit_sha: '123abc',
- source_project: project)
+ source_project: project)
# the first MR includes c1c67abba which is a cherry-pick of the fake picked_mr merge request
create(:track_mr_picking_note, noteable: picked_mr, project: project, commit_id: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478')
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index 4485ce585bb..c952bcddd9a 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -159,14 +159,37 @@ RSpec.describe Deployments::UpdateEnvironmentService do
{ name: 'production', auto_stop_in: '1 day' }
end
+ before do
+ environment.update_attribute(:auto_stop_at, nil)
+ end
+
it 'renews auto stop at' do
freeze_time do
- environment.update!(auto_stop_at: nil)
-
expect { subject.execute }
.to change { environment.reset.auto_stop_at&.round }.from(nil).to(1.day.since.round)
end
end
+
+ context 'when value is a variable' do
+ let(:options) { { name: 'production', auto_stop_in: '$TTL' } }
+
+ let(:yaml_variables) do
+ [
+ { key: "TTL", value: '2 days', public: true }
+ ]
+ end
+
+ before do
+ job.update_attribute(:yaml_variables, yaml_variables)
+ end
+
+ it 'renews auto stop at with expanded variable value' do
+ freeze_time do
+ expect { subject.execute }
+ .to change { environment.reset.auto_stop_at&.round }.from(nil).to(2.days.since.round)
+ end
+ end
+ end
end
context 'when deployment tier is specified' do
diff --git a/spec/services/design_management/delete_designs_service_spec.rb b/spec/services/design_management/delete_designs_service_spec.rb
index a0e049ea42a..48e53a92758 100644
--- a/spec/services/design_management/delete_designs_service_spec.rb
+++ b/spec/services/design_management/delete_designs_service_spec.rb
@@ -126,7 +126,8 @@ RSpec.describe DesignManagement::DeleteDesignsService do
end
it 'updates UsageData for removed designs' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_designs_removed_action).with(author: user)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_designs_removed_action)
+ .with(author: user, project: project)
run_service
end
@@ -171,6 +172,11 @@ RSpec.describe DesignManagement::DeleteDesignsService do
run_service
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_DESIGNS_REMOVED }
+ subject(:service_action) { run_service }
+ end
end
context 'more than one design is passed' do
diff --git a/spec/services/design_management/save_designs_service_spec.rb b/spec/services/design_management/save_designs_service_spec.rb
index b76c91fbac9..c69df5f2eb9 100644
--- a/spec/services/design_management/save_designs_service_spec.rb
+++ b/spec/services/design_management/save_designs_service_spec.rb
@@ -106,7 +106,8 @@ RSpec.describe DesignManagement::SaveDesignsService do
it 'creates a commit, an event in the activity stream and updates the creation count', :aggregate_failures do
counter = Gitlab::UsageDataCounters::DesignsCounter
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_designs_added_action).with(author: user)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_designs_added_action)
+ .with(author: user, project: project)
expect { run_service }
.to change { Event.count }.by(1)
@@ -119,6 +120,11 @@ RSpec.describe DesignManagement::SaveDesignsService do
)
end
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_DESIGNS_ADDED }
+ subject(:service_action) { run_service }
+ end
+
it 'can run the same command in parallel' do
parellism = 4
@@ -206,11 +212,17 @@ RSpec.describe DesignManagement::SaveDesignsService do
end
it 'updates UsageData for changed designs' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_designs_modified_action).with(author: user)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_designs_modified_action)
+ .with(author: user, project: project)
run_service
end
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_DESIGNS_MODIFIED }
+ subject(:service_action) { run_service }
+ end
+
it 'records the correct events' do
counter = Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }
diff --git a/spec/services/discussions/capture_diff_note_positions_service_spec.rb b/spec/services/discussions/capture_diff_note_positions_service_spec.rb
index 25e5f549bee..8ba54495d4c 100644
--- a/spec/services/discussions/capture_diff_note_positions_service_spec.rb
+++ b/spec/services/discussions/capture_diff_note_positions_service_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Discussions::CaptureDiffNotePositionsService do
def build_position(diff_refs, new_line: nil, old_line: nil)
path = 'files/markdown/ruby-style-guide.md'
- Gitlab::Diff::Position.new(old_path: path, new_path: path,
- new_line: new_line, old_line: old_line, diff_refs: diff_refs)
+ Gitlab::Diff::Position.new(
+ old_path: path, new_path: path, new_line: new_line, old_line: old_line, diff_refs: diff_refs)
end
def note_for(new_line: nil, old_line: nil)
diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb
index 3ed8a0b1da0..4f766b73710 100644
--- a/spec/services/environments/stop_service_spec.rb
+++ b/spec/services/environments/stop_service_spec.rb
@@ -193,7 +193,7 @@ RSpec.describe Environments::StopService do
end
it 'has active environment at first' do
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
context 'when user is a developer' do
@@ -201,21 +201,43 @@ RSpec.describe Environments::StopService do
project.add_developer(user)
end
+ context 'and merge request has associated created_environments' do
+ let!(:environment1) { create(:environment, project: project, merge_request: merge_request) }
+ let!(:environment2) { create(:environment, project: project, merge_request: merge_request) }
+
+ before do
+ subject
+ end
+
+ it 'stops the associated created_environments' do
+ expect(environment1.reload).to be_stopped
+ expect(environment2.reload).to be_stopped
+ end
+
+ it 'does not affect environments that are not associated to the merge request' do
+ expect(pipeline.environments_in_self_and_project_descendants.first.merge_request).to be_nil
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
+ end
+ end
+
it 'stops the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_stopping
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_stopping
end
context 'when pipeline is a branch pipeline for merge request' do
let(:pipeline) do
- create(:ci_pipeline, source: :push, project: project, sha: merge_request.diff_head_sha,
- merge_requests_as_head_pipeline: [merge_request])
+ create(:ci_pipeline,
+ source: :push,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
end
it 'does not stop the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
end
@@ -241,7 +263,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
end
@@ -265,7 +287,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index e66b413a5c9..06f0eb1efbc 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -420,9 +420,9 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
service.save_designs(author, create: [design])
expect_snowplow_event(
- category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
+ category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
action: 'create',
- namespace: design.project.namespace,
+ namespace: design.project.namespace,
user: author,
project: design.project,
label: 'design_users'
@@ -433,9 +433,9 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
service.save_designs(author, update: [design])
expect_snowplow_event(
- category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
+ category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
action: 'update',
- namespace: design.project.namespace,
+ namespace: design.project.namespace,
user: author,
project: design.project,
label: 'design_users'
@@ -481,9 +481,9 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
service.destroy_designs([design], author)
expect_snowplow_event(
- category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
+ category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
action: 'destroy',
- namespace: design.project.namespace,
+ namespace: design.project.namespace,
user: author,
project: design.project,
label: 'design_users'
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 5de1c0e27be..973ead28462 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -596,7 +596,7 @@ RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state do
end
end
- project.repository.multi_action(
+ project.repository.commit_files(
user, message: 'message', branch_name: branch, actions: actions
)
end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index 6280f1263c3..a9f5b07fef4 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::BranchPushService, services: true do
+RSpec.describe Git::BranchPushService, :use_clean_rails_redis_caching, services: true do
include RepoHelpers
let_it_be(:user) { create(:user) }
@@ -285,7 +285,7 @@ RSpec.describe Git::BranchPushService, services: true do
author_email: commit_author.email
)
- allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit)
+ allow(Commit).to receive(:build_from_sidekiq_hash)
.and_return(commit)
allow(project.repository).to receive(:commits_between).and_return([commit])
@@ -348,7 +348,7 @@ RSpec.describe Git::BranchPushService, services: true do
committed_date: commit_time
)
- allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit)
+ allow(Commit).to receive(:build_from_sidekiq_hash)
.and_return(commit)
allow(project.repository).to receive(:commits_between).and_return([commit])
@@ -387,7 +387,7 @@ RSpec.describe Git::BranchPushService, services: true do
allow(project.repository).to receive(:commits_between)
.and_return([closing_commit])
- allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit)
+ allow(Commit).to receive(:build_from_sidekiq_hash)
.and_return(closing_commit)
project.add_maintainer(commit_author)
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index 7e5d7066e89..878a5c4ccf0 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -9,9 +9,13 @@ RSpec.describe Git::WikiPushService, services: true do
let_it_be(:key_id) { create(:key, user: current_user).shell_id }
let(:wiki) { create(:project_wiki, user: current_user) }
- let(:git_wiki) { wiki.wiki }
+ let(:default_branch) { wiki.default_branch }
let(:repository) { wiki.repository }
+ before do
+ repository.create_if_not_exists(default_branch)
+ end
+
describe '#execute' do
it 'executes model-specific callbacks' do
expect(wiki).to receive(:after_post_receive)
@@ -351,7 +355,12 @@ RSpec.describe Git::WikiPushService, services: true do
# that have not gone through our services.
def write_new_page
- generate(:wiki_page_title).tap { |t| git_wiki.write_page(t, 'markdown', 'Hello', commit_details) }
+ generate(:wiki_page_title).tap do |t|
+ repository.create_file(
+ current_user, ::Wiki.sluggified_full_path(t, 'md'), 'Hello',
+ **commit_details
+ )
+ end
end
# We write something to the wiki-repo that is not a page - as, for example, an
@@ -368,15 +377,26 @@ RSpec.describe Git::WikiPushService, services: true do
def update_page(title, new_title = nil)
new_title = title unless new_title.present?
- page = git_wiki.page(title: title)
- git_wiki.update_page(page.path, new_title, 'markdown', 'Hey', commit_details)
+
+ old_path = ::Wiki.sluggified_full_path(title, 'md')
+ new_path = ::Wiki.sluggified_full_path(new_title, 'md')
+
+ repository.update_file(
+ current_user, new_path, 'Hey',
+ **commit_details.merge(previous_path: old_path)
+ )
end
def delete_page(page)
- wiki.delete_page(page, 'commit message')
+ repository.delete_file(current_user, page.path, **commit_details)
end
def commit_details
- create(:git_wiki_commit_details, author: current_user)
+ {
+ branch_name: default_branch,
+ message: "commit message",
+ author_email: current_user.email,
+ author_name: current_user.name
+ }
end
end
diff --git a/spec/services/google_cloud/enable_cloudsql_service_spec.rb b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
index e54e5a8d446..f267f6d3bc2 100644
--- a/spec/services/google_cloud/enable_cloudsql_service_spec.rb
+++ b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
@@ -23,6 +23,11 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
project.save!
end
+ after do
+ project.variables.destroy_all # rubocop:disable Cop/DestroyAll
+ project.save!
+ end
+
it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
expect(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
@@ -35,5 +40,22 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
expect(result[:status]).to eq(:success)
end
+
+ context 'when Google APIs raise an error' do
+ it 'returns error result' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ allow(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
+ allow(instance).to receive(:enable_compute).with('prj-prod')
+ allow(instance).to receive(:enable_service_networking).with('prj-prod')
+ allow(instance).to receive(:enable_cloud_sql_admin).with('prj-staging')
+ allow(instance).to receive(:enable_compute).with('prj-staging')
+ allow(instance).to receive(:enable_service_networking).with('prj-staging')
+ .and_raise(Google::Apis::Error.new('error'))
+ end
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('error')
+ end
+ end
end
end
diff --git a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
new file mode 100644
index 00000000000..b83037f80cd
--- /dev/null
+++ b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::FetchGoogleIpListService,
+ :use_clean_rails_memory_store_caching, :clean_gitlab_redis_rate_limiting do
+ include StubRequests
+
+ let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
+ let(:headers) { { 'Content-Type' => 'application/json' } }
+
+ subject { described_class.new.execute }
+
+ before do
+ WebMock.stub_request(:get, described_class::GOOGLE_IP_RANGES_URL)
+ .to_return(status: 200, body: google_cloud_ips, headers: headers)
+ end
+
+ describe '#execute' do
+ it 'returns a list of IPAddr subnets and caches the result' do
+ expect(::ObjectStorage::CDN::GoogleIpCache).to receive(:update!).and_call_original
+ expect(subject[:subnets]).to be_an(Array)
+ expect(subject[:subnets]).to all(be_an(IPAddr))
+ end
+
+ shared_examples 'IP range retrieval failure' do
+ it 'does not cache the result and logs an error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
+ expect(::ObjectStorage::CDN::GoogleIpCache).not_to receive(:update!)
+ expect(subject[:subnets]).to be_nil
+ end
+ end
+
+ context 'with rate limit in effect' do
+ before do
+ 10.times { described_class.new.execute }
+ end
+
+ it 'returns rate limit error' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq("#{described_class} was rate limited")
+ end
+ end
+
+ context 'when the URL returns a 404' do
+ before do
+ WebMock.stub_request(:get, described_class::GOOGLE_IP_RANGES_URL).to_return(status: 404)
+ end
+
+ it_behaves_like 'IP range retrieval failure'
+ end
+
+ context 'when the URL returns too large of a payload' do
+ before do
+ stub_const("#{described_class}::RESPONSE_BODY_LIMIT", 300)
+ end
+
+ it_behaves_like 'IP range retrieval failure'
+ end
+
+ context 'when the URL returns HTML' do
+ let(:headers) { { 'Content-Type' => 'text/html' } }
+
+ it_behaves_like 'IP range retrieval failure'
+ end
+
+ context 'when the URL returns empty results' do
+ let(:google_cloud_ips) { '{}' }
+
+ it_behaves_like 'IP range retrieval failure'
+ end
+ end
+end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 0cfde9ef434..0a8164c9ca3 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
it 'adds an onboarding progress record' do
- expect { subject }.to change(OnboardingProgress, :count).from(0).to(1)
+ expect { subject }.to change(Onboarding::Progress, :count).from(0).to(1)
end
context 'with before_commit callback' do
@@ -108,7 +108,7 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
it 'does not add an onboarding progress record' do
- expect { subject }.not_to change(OnboardingProgress, :count).from(0)
+ expect { subject }.not_to change(Onboarding::Progress, :count).from(0)
end
it_behaves_like 'has sync-ed traversal_ids'
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 0d699dd447b..9288793cc7a 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -35,17 +35,38 @@ RSpec.describe Groups::DestroyService do
it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) }
end
- context 'bot tokens', :sidekiq_might_not_need_inline do
- it 'removes group bot', :aggregate_failures do
- bot = create(:user, :project_bot)
- group.add_developer(bot)
- token = create(:personal_access_token, user: bot)
+ context 'bot tokens', :sidekiq_inline do
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates group bot removal', :aggregate_failures do
+ bot = create(:user, :project_bot)
+ group.add_developer(bot)
+ create(:personal_access_token, user: bot)
+
+ destroy_group(group, user, async)
+
+ expect(
+ Users::GhostUserMigration.where(user: bot,
+ initiator_user: user)
+ ).to be_exists
+ end
+ end
- destroy_group(group, user, async)
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'removes group bot', :aggregate_failures do
+ bot = create(:user, :project_bot)
+ group.add_developer(bot)
+ token = create(:personal_access_token, user: bot)
- expect(PersonalAccessToken.find_by(id: token.id)).to be_nil
- expect(User.find_by(id: bot.id)).to be_nil
- expect(User.find_by(id: user.id)).not_to be_nil
+ destroy_group(group, user, async)
+
+ expect(PersonalAccessToken.find_by(id: token.id)).to be_nil
+ expect(User.find_by(id: bot.id)).to be_nil
+ expect(User.find_by(id: user.id)).not_to be_nil
+ end
end
end
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index 292f2e2b86b..a4dfec4723a 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -149,9 +149,9 @@ RSpec.describe Groups::ImportExport::ImportService do
it 'logs the import success' do
expect(import_logger).to receive(:info).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: 'Group Import/Export: Import succeeded'
+ message: 'Group Import/Export: Import succeeded'
).once
subject
@@ -161,9 +161,9 @@ RSpec.describe Groups::ImportExport::ImportService do
context 'when user does not have correct permissions' do
it 'logs the error and raises an exception' do
expect(import_logger).to receive(:error).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: a_string_including('Errors occurred')
+ message: a_string_including('Errors occurred')
)
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
@@ -186,9 +186,9 @@ RSpec.describe Groups::ImportExport::ImportService do
it 'logs the error and raises an exception' do
expect(import_logger).to receive(:error).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: a_string_including('Errors occurred')
+ message: a_string_including('Errors occurred')
).once
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
@@ -267,9 +267,9 @@ RSpec.describe Groups::ImportExport::ImportService do
it 'logs the import success' do
expect(import_logger).to receive(:info).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: 'Group Import/Export: Import succeeded'
+ message: 'Group Import/Export: Import succeeded'
).once
subject
@@ -279,9 +279,9 @@ RSpec.describe Groups::ImportExport::ImportService do
context 'when user does not have correct permissions' do
it 'logs the error and raises an exception' do
expect(import_logger).to receive(:error).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: a_string_including('Errors occurred')
+ message: a_string_including('Errors occurred')
)
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
@@ -304,9 +304,9 @@ RSpec.describe Groups::ImportExport::ImportService do
it 'logs the error and raises an exception' do
expect(import_logger).to receive(:error).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: a_string_including('Errors occurred')
+ message: a_string_including('Errors occurred')
).once
expect { subject }.to raise_error(Gitlab::ImportExport::Error)
@@ -328,9 +328,9 @@ RSpec.describe Groups::ImportExport::ImportService do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
expect(import_logger).to receive(:info).with(
- group_id: group.id,
+ group_id: group.id,
group_name: group.name,
- message: 'Group Import/Export: Import succeeded'
+ message: 'Group Import/Export: Import succeeded'
)
subject
diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb
index 1c26677cfa5..67a2c237e43 100644
--- a/spec/services/import/github_service_spec.rb
+++ b/spec/services/import/github_service_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Import::GithubService do
end
context 'repository size validation' do
- let(:repository_double) { double(name: 'repository', size: 99) }
+ let(:repository_double) { { name: 'repository', size: 99 } }
before do
expect(client).to receive(:repository).and_return(repository_double)
@@ -84,7 +84,7 @@ RSpec.describe Import::GithubService do
end
it 'returns error when the repository is larger than the limit' do
- allow(repository_double).to receive(:size).and_return(101)
+ repository_double[:size] = 101
expect(subject.execute(access_params, :github)).to include(size_limit_error)
end
@@ -103,7 +103,7 @@ RSpec.describe Import::GithubService do
end
it 'returns error when the repository is larger than the limit' do
- allow(repository_double).to receive(:size).and_return(101)
+ repository_double[:size] = 101
expect(subject.execute(access_params, :github)).to include(size_limit_error)
end
@@ -113,14 +113,14 @@ RSpec.describe Import::GithubService do
context 'when import source is disabled' do
let(:repository_double) do
- double({
+ {
name: 'vim',
description: 'test',
full_name: 'test/vim',
clone_url: 'http://repo.com/repo/repo.git',
private: false,
has_wiki?: false
- })
+ }
end
before do
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 55e0e799c19..dc72cf04776 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Issuable::BulkUpdateService do
let(:bulk_update_params) do
{
- add_label_ids: add_labels.map(&:id),
+ add_label_ids: add_labels.map(&:id),
remove_label_ids: remove_labels.map(&:id)
}
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 304e4bb3ebb..838e0675372 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -63,12 +63,14 @@ RSpec.describe Issues::BuildService do
it 'wraps the note in a blockquote' do
note_text = "This is a string\n"\
+ "\n"\
">>>\n"\
"with a blockquote\n"\
"> That has a quote\n"\
">>>\n"
note_result = " > This is a string\n"\
" > \n"\
+ " > \n"\
" > > with a blockquote\n"\
" > > > That has a quote\n"\
" > \n"
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 80c455e72b0..4a84862b9d5 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -416,7 +416,7 @@ RSpec.describe Issues::CreateService do
context "when issuable feature is private" do
before do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
+ merge_requests_access_level: ProjectFeature::PRIVATE)
end
levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
@@ -555,24 +555,29 @@ RSpec.describe Issues::CreateService do
expect(reloaded_discussion.last_note.system).to eq(true)
end
- it 'assigns the title and description for the issue' do
- issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ it 'sets default title and description values if not provided' do
+ issue = described_class.new(
+ project: project, current_user: user,
+ params: opts,
+ spam_params: spam_params
+ ).execute
- expect(issue.title).not_to be_nil
- expect(issue.description).not_to be_nil
+ expect(issue).to be_persisted
+ expect(issue.title).to eq("Follow-up from \"#{merge_request.title}\"")
+ expect(issue.description).to include("The following discussion from #{merge_request.to_reference} should be addressed")
end
- it 'can set nil explicitly to the title and description' do
+ it 'takes params from the request over the default values' do
issue = described_class.new(project: project, current_user: user,
- params: {
- merge_request_to_resolve_discussions_of: merge_request,
- description: nil,
- title: nil
- },
+ params: opts.merge(
+ description: 'Custom issue description',
+ title: 'My new issue'
+ ),
spam_params: spam_params).execute
- expect(issue.description).to be_nil
- expect(issue.title).to be_nil
+ expect(issue).to be_persisted
+ expect(issue.description).to eq('Custom issue description')
+ expect(issue.title).to eq('My new issue')
end
end
@@ -594,24 +599,29 @@ RSpec.describe Issues::CreateService do
expect(reloaded_discussion.last_note.system).to eq(true)
end
- it 'assigns the title and description for the issue' do
- issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ it 'sets default title and description values if not provided' do
+ issue = described_class.new(
+ project: project, current_user: user,
+ params: opts,
+ spam_params: spam_params
+ ).execute
- expect(issue.title).not_to be_nil
- expect(issue.description).not_to be_nil
+ expect(issue).to be_persisted
+ expect(issue.title).to eq("Follow-up from \"#{merge_request.title}\"")
+ expect(issue.description).to include("The following discussion from #{merge_request.to_reference} should be addressed")
end
- it 'can set nil explicitly to the title and description' do
+ it 'takes params from the request over the default values' do
issue = described_class.new(project: project, current_user: user,
- params: {
- merge_request_to_resolve_discussions_of: merge_request,
- description: nil,
- title: nil
- },
+ params: opts.merge(
+ description: 'Custom issue description',
+ title: 'My new issue'
+ ),
spam_params: spam_params).execute
- expect(issue.description).to be_nil
- expect(issue.title).to be_nil
+ expect(issue).to be_persisted
+ expect(issue.description).to eq('Custom issue description')
+ expect(issue.title).to eq('My new issue')
end
end
end
diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb
index 20064bd7e4b..37a94e1d6a2 100644
--- a/spec/services/issues/relative_position_rebalancing_service_spec.rb
+++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb
@@ -72,22 +72,8 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
end.not_to change { issues_in_position_order.map(&:id) }
end
- it 'does nothing if the feature flag is disabled' do
- stub_feature_flags(rebalance_issues: false)
- issue = project.issues.first
- issue.project
- issue.project.group
- old_pos = issue.relative_position
-
- # fetching root namespace in the initializer triggers 2 queries:
- # for fetching a random project from collection and fetching the root namespace.
- expect { service.execute }.not_to exceed_query_limit(2)
- expect(old_pos).to eq(issue.reload.relative_position)
- end
-
it 'acts if the flag is enabled for the root namespace' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
- stub_feature_flags(rebalance_issues: project.root_namespace)
expect { service.execute }.to change { issue.reload.relative_position }
end
@@ -95,7 +81,6 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
it 'acts if the flag is enabled for the group' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
project.update!(group: create(:group))
- stub_feature_flags(rebalance_issues: issue.project.group)
expect { service.execute }.to change { issue.reload.relative_position }
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index aef3608831c..8a2e9ed74f7 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -48,6 +48,11 @@ RSpec.describe Issues::UpdateService, :mailer do
described_class.new(project: project, current_user: user, params: opts).execute(issue)
end
+ it_behaves_like 'issuable update service updating last_edited_at values' do
+ let(:issuable) { issue }
+ subject(:update_issuable) { update_issue(update_params) }
+ end
+
context 'valid params' do
let(:opts) do
{
@@ -299,38 +304,6 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
- it 'does not rebalance even if needed if the flag is disabled' do
- stub_feature_flags(rebalance_issues: false)
-
- range = described_class::NO_REBALANCING_NEEDED
- issue1 = create(:issue, project: project, relative_position: range.first - 100)
- issue2 = create(:issue, project: project, relative_position: range.first)
- issue.update!(relative_position: RelativePositioning::START_POSITION)
-
- opts[:move_between_ids] = [issue1.id, issue2.id]
-
- expect(Issues::RebalancingWorker).not_to receive(:perform_async)
-
- update_issue(opts)
- expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
- end
-
- it 'rebalances if needed if the flag is enabled for the project' do
- stub_feature_flags(rebalance_issues: project)
-
- range = described_class::NO_REBALANCING_NEEDED
- issue1 = create(:issue, project: project, relative_position: range.first - 100)
- issue2 = create(:issue, project: project, relative_position: range.first)
- issue.update!(relative_position: RelativePositioning::START_POSITION)
-
- opts[:move_between_ids] = [issue1.id, issue2.id]
-
- expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
-
- update_issue(opts)
- expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
- end
-
it 'rebalances if needed on the left' do
range = described_class::NO_REBALANCING_NEEDED
issue1 = create(:issue, project: project, relative_position: range.first - 100)
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index fe9f3ddc14d..25696ca209e 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -20,10 +20,10 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
case source
when Project
source.add_maintainer(user)
- OnboardingProgress.onboard(source.namespace)
+ Onboarding::Progress.onboard(source.namespace)
when Group
source.add_owner(user)
- OnboardingProgress.onboard(source)
+ Onboarding::Progress.onboard(source)
end
end
@@ -59,7 +59,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'adds a user to members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
context 'when user_id is passed as an integer' do
@@ -68,7 +68,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'successfully creates member' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -78,7 +78,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'successfully creates members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include(member, user_invited_by_id)
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -88,7 +88,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'successfully creates members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include(member, user_invited_by_id)
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -98,7 +98,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'adds a user to members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include member
- expect(OnboardingProgress.completed?(source, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source, :user_added)).to be(true)
end
it 'triggers a members added event' do
@@ -119,7 +119,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to be_present
expect(source.users).not_to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -130,7 +130,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to be_present
expect(source.users).not_to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -141,7 +141,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to include("#{member.username}: Access level is not included in the list")
expect(source.users).not_to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -153,7 +153,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'allows already invited members to be re-invited by email and updates the member access' do
expect(execute_service[:status]).to eq(:success)
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -170,7 +170,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'does not update the member' do
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to eq("#{project_bot.username}: not authorized to update member")
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -178,7 +178,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'adds the member' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include project_bot
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
end
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index ab98fad5d73..0846ec7f50e 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -36,29 +36,15 @@ RSpec.describe MergeRequests::ApprovalService do
it 'does not publish MergeRequests::ApprovedEvent' do
expect { service.execute(merge_request) }.not_to publish_event(MergeRequests::ApprovedEvent)
end
+ end
- context 'async_after_approval feature flag is disabled' do
- before do
- stub_feature_flags(async_after_approval: false)
- end
-
- it 'does not create approve MR event' do
- expect(EventCreateService).not_to receive(:new)
-
- service.execute(merge_request)
- end
-
- it 'does not create an approval note' do
- expect(SystemNoteService).not_to receive(:approve_mr)
-
- service.execute(merge_request)
- end
-
- it 'does not mark pending todos as done' do
- service.execute(merge_request)
+ context 'with an already approved MR' do
+ before do
+ merge_request.approvals.create!(user: user)
+ end
- expect(todo.reload).to be_pending
- end
+ it 'does not create an approval' do
+ expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end
end
@@ -81,51 +67,6 @@ RSpec.describe MergeRequests::ApprovalService do
.to publish_event(MergeRequests::ApprovedEvent)
.with(current_user_id: user.id, merge_request_id: merge_request.id)
end
-
- context 'async_after_approval feature flag is disabled' do
- let(:notification_service) { NotificationService.new }
-
- before do
- stub_feature_flags(async_after_approval: false)
- allow(service).to receive(:notification_service).and_return(notification_service)
- end
-
- it 'creates approve MR event' do
- expect_next_instance_of(EventCreateService) do |instance|
- expect(instance).to receive(:approve_mr)
- .with(merge_request, user)
- end
-
- service.execute(merge_request)
- end
-
- it 'creates an approval note' do
- expect(SystemNoteService).to receive(:approve_mr).with(merge_request, user)
-
- service.execute(merge_request)
- end
-
- it 'marks pending todos as done' do
- service.execute(merge_request)
-
- expect(todo.reload).to be_done
- end
-
- it 'sends a notification when approving' do
- expect(notification_service).to receive_message_chain(:async, :approve_mr)
- .with(merge_request, user)
-
- service.execute(merge_request)
- end
-
- context 'with remaining approvals' do
- it 'fires an approval webhook' do
- expect(service).to receive(:execute_hooks).with(merge_request, 'approved')
-
- service.execute(merge_request)
- end
- end
- end
end
context 'user cannot update the merge request' do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 3c9d2271ddc..6a6f01e6a95 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -20,18 +20,30 @@ RSpec.describe MergeRequests::BuildService do
let(:merge_request) { service.execute }
let(:compare) { double(:compare, commits: commits) }
let(:commit_1) do
- double(:commit_1, sha: 'f00ba6', safe_message: 'Initial commit',
- gitaly_commit?: false, id: 'f00ba6', parent_ids: ['f00ba5'])
+ double(:commit_1,
+ sha: 'f00ba6',
+ safe_message: 'Initial commit',
+ gitaly_commit?: false,
+ id: 'f00ba6',
+ parent_ids: ['f00ba5'])
end
let(:commit_2) do
- double(:commit_2, sha: 'f00ba7', safe_message: "Closes #1234 Second commit\n\nCreate the app",
- gitaly_commit?: false, id: 'f00ba7', parent_ids: ['f00ba6'])
+ double(:commit_2,
+ sha: 'f00ba7',
+ safe_message: "Closes #1234 Second commit\n\nCreate the app",
+ gitaly_commit?: false,
+ id: 'f00ba7',
+ parent_ids: ['f00ba6'])
end
let(:commit_3) do
- double(:commit_3, sha: 'f00ba8', safe_message: 'This is a bad commit message!',
- gitaly_commit?: false, id: 'f00ba8', parent_ids: ['f00ba7'])
+ double(:commit_3,
+ sha: 'f00ba8',
+ safe_message: 'This is a bad commit message!',
+ gitaly_commit?: false,
+ id: 'f00ba8',
+ parent_ids: ['f00ba7'])
end
let(:commits) { nil }
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index c443d758a77..dc96b5c0e5e 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreatePipelineService do
+RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache do
include ProjectForksHelper
let_it_be(:project, reload: true) { create(:project, :repository) }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 9c9bcb79990..4102cdc101e 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -434,7 +434,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
context "when issuable feature is private" do
before do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
+ merge_requests_access_level: ProjectFeature::PRIVATE)
end
levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index 24a1a8b3113..aa5d6dcd1fb 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -75,6 +75,7 @@ RSpec.describe MergeRequests::FfMergeService do
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
expect { execute_ff_merge }.not_to change { merge_request.squash_commit_sha }
+ expect(merge_request.merge_commit_sha).to be_nil
expect(merge_request.in_progress_merge_commit_sha).to be_nil
end
@@ -87,6 +88,7 @@ RSpec.describe MergeRequests::FfMergeService do
.to change { merge_request.squash_commit_sha }
.from(nil)
+ expect(merge_request.merge_commit_sha).to be_nil
expect(merge_request.in_progress_merge_commit_sha).to be_nil
end
end
@@ -106,7 +108,6 @@ RSpec.describe MergeRequests::FfMergeService do
service.execute(merge_request)
- expect(merge_request.merge_error).to include(error_message)
expect(Gitlab::AppLogger).to have_received(:error).with(a_string_matching(error_message))
end
@@ -117,11 +118,6 @@ RSpec.describe MergeRequests::FfMergeService do
pre_receive_error = Gitlab::Git::PreReceiveError.new(raw_message, fallback_message: error_message)
allow(service).to receive(:repository).and_raise(pre_receive_error)
allow(service).to receive(:execute_hooks)
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- pre_receive_error,
- pre_receive_message: raw_message,
- merge_request_id: merge_request.id
- )
service.execute(merge_request)
diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
index c43f5db6059..3db3efedb84 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do
end
context 'when execute_hooks option is set to true' do
- let(:options) { { execute_hooks: true } }
+ let(:options) { { 'execute_hooks' => true } }
it 'executes hooks and integrations' do
expect(merge_request.project).to receive(:execute_hooks).with(anything, :merge_request_hooks)
diff --git a/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb b/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb
new file mode 100644
index 00000000000..5722bb79cc5
--- /dev/null
+++ b/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::MergeRequests::Mergeability::DetailedMergeStatusService do
+ subject(:detailed_merge_status) { described_class.new(merge_request: merge_request).execute }
+
+ context 'when merge status is cannot_be_merged_rechecking' do
+ let(:merge_request) { create(:merge_request, merge_status: :cannot_be_merged_rechecking) }
+
+ it 'returns :checking' do
+ expect(detailed_merge_status).to eq(:checking)
+ end
+ end
+
+ context 'when merge status is preparing' do
+ let(:merge_request) { create(:merge_request, merge_status: :preparing) }
+
+ it 'returns :checking' do
+ expect(detailed_merge_status).to eq(:checking)
+ end
+ end
+
+ context 'when merge status is checking' do
+ let(:merge_request) { create(:merge_request, merge_status: :checking) }
+
+ it 'returns :checking' do
+ expect(detailed_merge_status).to eq(:checking)
+ end
+ end
+
+ context 'when merge status is unchecked' do
+ let(:merge_request) { create(:merge_request, merge_status: :unchecked) }
+
+ it 'returns :unchecked' do
+ expect(detailed_merge_status).to eq(:unchecked)
+ end
+ end
+
+ context 'when merge checks are a success' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns :mergeable' do
+ expect(detailed_merge_status).to eq(:mergeable)
+ end
+ end
+
+ context 'when merge status have a failure' do
+ let(:merge_request) { create(:merge_request) }
+
+ before do
+ merge_request.close!
+ end
+
+ it 'returns the failure reason' do
+ expect(detailed_merge_status).to eq(:not_open)
+ end
+ end
+
+ context 'when all but the ci check fails' do
+ let(:merge_request) { create(:merge_request) }
+
+ before do
+ merge_request.project.update!(only_allow_merge_if_pipeline_succeeds: true)
+ end
+
+ context 'when pipeline does not exist' do
+ it 'returns the failure reason' do
+ expect(detailed_merge_status).to eq(:ci_must_pass)
+ end
+ end
+
+ context 'when pipeline exists' do
+ before do
+ create(:ci_pipeline, ci_status, merge_request: merge_request,
+ project: merge_request.project, sha: merge_request.source_branch_sha,
+ head_pipeline_of: merge_request)
+ end
+
+ context 'when the pipeline is running' do
+ let(:ci_status) { :running }
+
+ it 'returns the failure reason' do
+ expect(detailed_merge_status).to eq(:ci_still_running)
+ end
+ end
+
+ context 'when the pipeline is not running' do
+ let(:ci_status) { :failed }
+
+ it 'returns the failure reason' do
+ expect(detailed_merge_status).to eq(:ci_must_pass)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/mergeability/logger_spec.rb b/spec/services/merge_requests/mergeability/logger_spec.rb
new file mode 100644
index 00000000000..a4d544884b9
--- /dev/null
+++ b/spec/services/merge_requests/mergeability/logger_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::Mergeability::Logger, :request_store do
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ subject(:logger) { described_class.new(merge_request: merge_request) }
+
+ let(:caller_id) { 'a' }
+
+ before do
+ allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id)
+ end
+
+ def loggable_data(**extras)
+ {
+ 'mergeability.expensive_operation.duration_s.values' => a_kind_of(Array),
+ "mergeability_merge_request_id" => merge_request.id,
+ "correlation_id" => a_kind_of(String),
+ "mergeability_project_id" => merge_request.project.id
+ }.merge(extras)
+ end
+
+ describe '#instrument' do
+ let(:operation_count) { 1 }
+
+ context 'when enabled' do
+ it "returns the block's value" do
+ expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
+ end
+
+ it 'records durations of instrumented operations' do
+ expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
+ expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data)))
+ end
+
+ expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
+
+ logger.commit
+ end
+
+ context 'with multiple observations' do
+ let(:operation_count) { 2 }
+
+ it 'records durations of instrumented operations' do
+ expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
+ expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data)))
+ end
+
+ 2.times do
+ expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
+ end
+
+ logger.commit
+ end
+ end
+
+ context 'when its a query' do
+ let(:extra_data) do
+ {
+ 'mergeability.expensive_operation.db_count.values' => a_kind_of(Array),
+ 'mergeability.expensive_operation.db_main_count.values' => a_kind_of(Array),
+ 'mergeability.expensive_operation.db_main_duration_s.values' => a_kind_of(Array),
+ 'mergeability.expensive_operation.db_primary_count.values' => a_kind_of(Array),
+ 'mergeability.expensive_operation.db_primary_duration_s.values' => a_kind_of(Array)
+ }
+ end
+
+ context 'with a single query' do
+ it 'includes SQL metrics' do
+ expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
+ expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
+ end
+
+ expect(logger.instrument(mergeability_name: :expensive_operation) { MergeRequest.count }).to eq(1)
+
+ logger.commit
+ end
+ end
+
+ context 'with multiple queries' do
+ it 'includes SQL metrics' do
+ expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
+ expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
+ end
+
+ expect(logger.instrument(mergeability_name: :expensive_operation) { Project.count + MergeRequest.count })
+ .to eq(2)
+
+ logger.commit
+ end
+ end
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ stub_feature_flags(mergeability_checks_logger: false)
+ end
+
+ it "returns the block's value" do
+ expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
+ end
+
+ it 'does not call the logger' do
+ expect(Gitlab::AppJsonLogger).not_to receive(:new)
+
+ expect(logger.instrument(mergeability_name: :expensive_operation) { Project.count + MergeRequest.count })
+ .to eq(2)
+
+ logger.commit
+ end
+ end
+
+ it 'raises an error when block is not provided' do
+ expect { logger.instrument(mergeability_name: :expensive_operation) }
+ .to raise_error(ArgumentError, 'block not given')
+ end
+ end
+end
diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
index afea3e952a1..cf34923795e 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -69,6 +69,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result)
end
+ expect_next_instance_of(MergeRequests::Mergeability::Logger, merge_request: merge_request) do |logger|
+ expect(logger).to receive(:instrument).with(mergeability_name: 'check_ci_status_service').and_call_original
+ expect(logger).to receive(:commit)
+ end
+
expect(execute.success?).to eq(true)
end
end
@@ -80,6 +85,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true)
end
+ expect_next_instance_of(MergeRequests::Mergeability::Logger, merge_request: merge_request) do |logger|
+ expect(logger).to receive(:instrument).with(mergeability_name: 'check_ci_status_service').and_call_original
+ expect(logger).to receive(:commit)
+ end
+
expect(execute.success?).to eq(true)
end
end
diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb
index f5f6f0ca301..3a0b17c2768 100644
--- a/spec/services/merge_requests/update_assignees_service_spec.rb
+++ b/spec/services/merge_requests/update_assignees_service_spec.rb
@@ -113,49 +113,6 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
expect { service.execute(merge_request) }
.to issue_fewer_queries_than { update_service.execute(other_mr) }
end
-
- context 'setting state of assignees' do
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it 'does not set state as attention_requested if feature flag is disabled' do
- update_merge_request
-
- expect(merge_request.merge_request_assignees[0].state).not_to eq('attention_requested')
- end
-
- context 'feature flag is enabled for current_user' do
- before do
- stub_feature_flags(mr_attention_requests: user)
- end
-
- it 'sets state as attention_requested' do
- update_merge_request
-
- expect(merge_request.merge_request_assignees[0].state).to eq('attention_requested')
- expect(merge_request.merge_request_assignees[0].updated_state_by).to eq(user)
- end
-
- it 'uses reviewers state if it is same user as new assignee' do
- merge_request.reviewers << user2
-
- update_merge_request
-
- expect(merge_request.merge_request_assignees[0].state).to eq('unreviewed')
- end
-
- context 'when assignee_ids matches existing assignee' do
- let(:opts) { { assignee_ids: [user3.id] } }
-
- it 'keeps original assignees state' do
- update_merge_request
-
- expect(merge_request.find_assignee(user3).state).to eq('unreviewed')
- end
- end
- end
- end
end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index b7fb48718d8..8ebabd64d8a 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -47,6 +47,11 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
@merge_request.reload
end
+ it_behaves_like 'issuable update service updating last_edited_at values' do
+ let(:issuable) { merge_request }
+ subject(:update_issuable) { update_merge_request(update_params) }
+ end
+
context 'valid params' do
let(:locked) { true }
@@ -215,14 +220,6 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
end
-
- it 'updates attention requested by of reviewer' do
- opts[:reviewers] = [user2]
-
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
-
- expect(merge_request.find_reviewer(user2).updated_state_by).to eq(user)
- end
end
context 'when reviewers did not change' do
@@ -328,49 +325,6 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
update_merge_request(reviewer_ids: [user.id])
end
-
- context 'setting state of reviewers' do
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it 'does not set state as attention_requested if feature flag is disabled' do
- update_merge_request(reviewer_ids: [user.id])
-
- expect(merge_request.merge_request_reviewers[0].state).not_to eq('attention_requested')
- end
-
- context 'feature flag is enabled for current_user' do
- before do
- stub_feature_flags(mr_attention_requests: user)
- end
-
- it 'sets state as attention_requested' do
- update_merge_request(reviewer_ids: [user2.id])
-
- expect(merge_request.merge_request_reviewers[0].state).to eq('attention_requested')
- expect(merge_request.merge_request_reviewers[0].updated_state_by).to eq(user)
- end
-
- it 'keeps original reviewers state' do
- merge_request.find_reviewer(user2).update!(state: :unreviewed)
-
- update_merge_request({
- reviewer_ids: [user2.id]
- })
-
- expect(merge_request.find_reviewer(user2).state).to eq('unreviewed')
- end
-
- it 'uses reviewers state if it is same user as new assignee' do
- merge_request.assignees << user
-
- update_merge_request(reviewer_ids: [user.id])
-
- expect(merge_request.merge_request_reviewers[0].state).to eq('unreviewed')
- end
- end
- end
end
it 'creates a resource label event' do
@@ -561,9 +515,9 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
before do
create(:ci_pipeline,
project: project,
- ref: merge_request.source_branch,
- sha: merge_request.diff_head_sha,
- status: :success)
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: :success)
perform_enqueued_jobs do
@merge_request = service.execute(merge_request)
@@ -895,7 +849,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
@merge_request = described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
- should_not_email(subscriber)
+ should_email(subscriber)
should_not_email(non_subscriber)
end
@@ -1133,53 +1087,6 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
end
end
-
- context 'setting state of assignees' do
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it 'does not set state as attention_requested if feature flag is disabled' do
- update_merge_request({
- assignee_ids: [user2.id]
- })
-
- expect(merge_request.merge_request_assignees[0].state).not_to eq('attention_requested')
- end
-
- context 'feature flag is enabled for current_user' do
- before do
- stub_feature_flags(mr_attention_requests: user)
- end
-
- it 'sets state as attention_requested' do
- update_merge_request({
- assignee_ids: [user2.id]
- })
-
- expect(merge_request.merge_request_assignees[0].state).to eq('attention_requested')
- expect(merge_request.merge_request_assignees[0].updated_state_by).to eq(user)
- end
-
- it 'keeps original assignees state' do
- update_merge_request({
- assignee_ids: [user3.id]
- })
-
- expect(merge_request.find_assignee(user3).state).to eq('unreviewed')
- end
-
- it 'uses reviewers state if it is same user as new assignee' do
- merge_request.reviewers << user2
-
- update_merge_request({
- assignee_ids: [user2.id]
- })
-
- expect(merge_request.merge_request_assignees[0].state).to eq('unreviewed')
- end
- end
- end
end
context 'when adding time spent' do
diff --git a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
index b326fc1726d..47e5557105b 100644
--- a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memor
start_branch: project.default_branch,
encoding: 'text',
file_path: ".gitlab/dashboards/custom_dashboard.yml",
- file_content: file_content_hash.to_yaml
+ file_content: file_content_hash.to_yaml
}
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 37318d76586..4922e72b7a4 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -134,8 +134,7 @@ RSpec.describe Notes::CreateService do
context 'in a merge request' do
let_it_be(:project_with_repo) { create(:project, :repository) }
let_it_be(:merge_request) do
- create(:merge_request, source_project: project_with_repo,
- target_project: project_with_repo)
+ create(:merge_request, source_project: project_with_repo, target_project: project_with_repo)
end
context 'noteable highlight cache clearing' do
@@ -181,8 +180,7 @@ RSpec.describe Notes::CreateService do
it 'does not clear cache when note is not the first of the discussion' do
prev_note =
- create(:diff_note_on_merge_request, noteable: merge_request,
- project: project_with_repo)
+ create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo)
reply_opts =
opts.merge(in_reply_to_discussion_id: prev_note.discussion_id,
type: 'DiffNote',
@@ -408,9 +406,9 @@ RSpec.describe Notes::CreateService do
expect(issuable.draft?).to eq(can_use_quick_action)
}
),
- # Remove draft status
+ # Remove draft (set ready) status
QuickAction.new(
- action_text: "/draft",
+ action_text: "/ready",
before_action: -> {
issuable.reload.update!(title: "Draft: title")
},
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index be95a4bb181..82caec52aee 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -57,13 +57,11 @@ RSpec.describe Notes::DestroyService do
context 'in a merge request' do
let_it_be(:repo_project) { create(:project, :repository) }
let_it_be(:merge_request) do
- create(:merge_request, source_project: repo_project,
- target_project: repo_project)
+ create(:merge_request, source_project: repo_project, target_project: repo_project)
end
let_it_be(:note) do
- create(:diff_note_on_merge_request, project: repo_project,
- noteable: merge_request)
+ create(:diff_note_on_merge_request, project: repo_project, noteable: merge_request)
end
it 'does not track issue comment removal usage data' do
@@ -84,9 +82,8 @@ RSpec.describe Notes::DestroyService do
end
it 'does not clear cache when note is not the first of the discussion' do
- reply_note = create(:diff_note_on_merge_request, in_reply_to: note,
- project: repo_project,
- noteable: merge_request)
+ reply_note = create(:diff_note_on_merge_request,
+ in_reply_to: note, project: repo_project, noteable: merge_request)
expect(merge_request).not_to receive(:diffs)
diff --git a/spec/services/onboarding_progress_service_spec.rb b/spec/services/onboarding/progress_service_spec.rb
index ef4f4f0d822..e9b8ea2e859 100644
--- a/spec/services/onboarding_progress_service_spec.rb
+++ b/spec/services/onboarding/progress_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe OnboardingProgressService do
+RSpec.describe Onboarding::ProgressService do
describe '.async' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:action) { :git_pull }
@@ -19,12 +19,12 @@ RSpec.describe OnboardingProgressService do
context 'when onboarded' do
before do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
end
context 'when action is already completed' do
before do
- OnboardingProgress.register(namespace, action)
+ Onboarding::Progress.register(namespace, action)
end
it 'does not schedule a worker' do
@@ -52,13 +52,13 @@ RSpec.describe OnboardingProgressService do
context 'when the namespace is a root' do
before do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
end
it 'registers a namespace onboarding progress action for the given namespace' do
execute_service
- expect(OnboardingProgress.completed?(namespace, :subscription_created)).to eq(true)
+ expect(Onboarding::Progress.completed?(namespace, :subscription_created)).to eq(true)
end
end
@@ -66,13 +66,13 @@ RSpec.describe OnboardingProgressService do
let(:group) { create(:group, :nested) }
before do
- OnboardingProgress.onboard(group)
+ Onboarding::Progress.onboard(group)
end
it 'does not register a namespace onboarding progress action' do
execute_service
- expect(OnboardingProgress.completed?(group, :subscription_created)).to be(nil)
+ expect(Onboarding::Progress.completed?(group, :subscription_created)).to be(nil)
end
end
@@ -82,7 +82,7 @@ RSpec.describe OnboardingProgressService do
it 'does not register a namespace onboarding progress action' do
execute_service
- expect(OnboardingProgress.completed?(namespace, :subscription_created)).to be(nil)
+ expect(Onboarding::Progress.completed?(namespace, :subscription_created)).to be(nil)
end
end
end
diff --git a/spec/services/packages/debian/parse_debian822_service_spec.rb b/spec/services/packages/debian/parse_debian822_service_spec.rb
index ff146fda250..a2731816459 100644
--- a/spec/services/packages/debian/parse_debian822_service_spec.rb
+++ b/spec/services/packages/debian/parse_debian822_service_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
'Depends' => '${shlibs:Depends}, ${misc:Depends}',
'Description' => "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph."
},
- 'Package: sample-udeb' => {
+ 'Package: sample-udeb' => {
'Package' => 'sample-udeb',
'Package-Type' => 'udeb',
'Architecture' => 'any',
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
index 52bcddb6f5e..a45dd68cd6e 100644
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ b/spec/services/packages/debian/process_changes_service_spec.rb
@@ -5,7 +5,8 @@ RSpec.describe Packages::Debian::ProcessChangesService do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
- let_it_be(:incoming) { create(:debian_incoming, project: distribution.project) }
+
+ let!(:incoming) { create(:debian_incoming, project: distribution.project) }
let(:package_file) { incoming.package_files.last }
@@ -33,10 +34,11 @@ RSpec.describe Packages::Debian::ProcessChangesService do
existing_package.update!(debian_distribution: distribution)
end
- it 'does not create a package' do
+ it 'does not create a package and assigns the package_file to the existing package' do
expect { subject.execute }
.to not_change { Packages::Package.count }
.and not_change { Packages::PackageFile.count }
+ .and change(package_file, :package).to(existing_package)
end
context 'marked as pending_destruction' do
diff --git a/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb b/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb
new file mode 100644
index 00000000000..0fb58cc27d5
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BaseBuilder do
+ describe '#execute' do
+ subject { described_class.new.execute }
+
+ before do
+ stub_const("#{described_class}::ROOT_TAG", 'test')
+ stub_const("#{described_class}::ROOT_ATTRIBUTES", { foo1: 'bar1', foo2: 'bar2' })
+ end
+
+ it 'generate valid xml' do
+ result = Nokogiri::XML::Document.parse(subject)
+
+ expect(result.children.count).to eq(1)
+ expect(result.children.first.attributes.count).to eq(2)
+ expect(result.children.first.attributes['foo1'].value).to eq('bar1')
+ expect(result.children.first.attributes['foo2'].value).to eq('bar2')
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb
new file mode 100644
index 00000000000..2feb44c7c1b
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXml do
+ describe '#execute' do
+ subject { described_class.new.execute }
+
+ context "when generate empty xml" do
+ let(:expected_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <filelists xmlns="http://linux.duke.edu/metadata/filelists" packages="0"/>
+ XML
+ end
+
+ it 'generate expected xml' do
+ expect(subject).to eq(expected_xml)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb
new file mode 100644
index 00000000000..823aa18808a
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXml do
+ describe '#execute' do
+ subject { described_class.new.execute }
+
+ context "when generate empty xml" do
+ let(:expected_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
+ XML
+ end
+
+ it 'generate expected xml' do
+ expect(subject).to eq(expected_xml)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb
new file mode 100644
index 00000000000..f5294d6f7f7
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXml do
+ describe '#execute' do
+ subject { described_class.new.execute }
+
+ context "when generate empty xml" do
+ let(:expected_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
+ XML
+ end
+
+ it 'generate expected xml' do
+ expect(subject).to eq(expected_xml)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb
new file mode 100644
index 00000000000..29b0f73e3c1
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXml do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ let(:data) do
+ {
+ filelists: {
+ checksum: { type: "sha256", value: "123" },
+ 'open-checksum': { type: "sha256", value: "123" },
+ location: { href: "repodata/123-filelists.xml.gz" },
+ timestamp: { value: 1644602784 },
+ size: { value: 11111 },
+ 'open-size': { value: 11111 }
+ },
+ primary: {
+ checksum: { type: "sha256", value: "234" },
+ 'open-checksum': { type: "sha256", value: "234" },
+ location: { href: "repodata/234-primary.xml.gz" },
+ timestamp: { value: 1644602784 },
+ size: { value: 22222 },
+ 'open-size': { value: 22222 }
+ },
+ other: {
+ checksum: { type: "sha256", value: "345" },
+ 'open-checksum': { type: "sha256", value: "345" },
+ location: { href: "repodata/345-other.xml.gz" },
+ timestamp: { value: 1644602784 },
+ size: { value: 33333 },
+ 'open-size': { value: 33333 }
+ }
+ }
+ end
+
+ let(:creation_timestamp) { 111111 }
+
+ before do
+ allow(Time).to receive(:now).and_return(creation_timestamp)
+ end
+
+ it 'generate valid xml' do
+ # Have one root attribute
+ result = Nokogiri::XML::Document.parse(subject)
+ expect(result.children.count).to eq(1)
+
+ # Root attribute name is 'repomd'
+ root = result.children.first
+ expect(root.name).to eq('repomd')
+
+ # Have the same count of 'data' tags as count of keys in 'data'
+ expect(result.css('data').count).to eq(data.count)
+ end
+
+ it 'has all data info' do
+ result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
+
+ data.each do |tag_name, tag_attributes|
+ tag_attributes.each_key do |key|
+ expect(result.at("//repomd/data[@type=\"#{tag_name}\"]/#{key}")).not_to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb
index 3f4d37e5ddc..aa955b3445b 100644
--- a/spec/services/post_receive_service_spec.rb
+++ b/spec/services/post_receive_service_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe PostReceiveService do
+ include GitlabShellHelpers
include Gitlab::Routing
let_it_be(:user) { create(:user) }
@@ -13,7 +14,6 @@ RSpec.describe PostReceiveService do
let(:identifier) { 'key-123' }
let(:gl_repository) { "project-#{project.id}" }
let(:branch_name) { 'feature' }
- let(:secret_token) { Gitlab::Shell.secret_token }
let(:reference_counter) { double('ReferenceCounter') }
let(:push_options) { ['ci.skip', 'another push option'] }
let(:repository) { project.repository }
@@ -25,7 +25,6 @@ RSpec.describe PostReceiveService do
let(:params) do
{
gl_repository: gl_repository,
- secret_token: secret_token,
identifier: identifier,
changes: changes,
push_options: push_options
diff --git a/spec/services/projects/blame_service_spec.rb b/spec/services/projects/blame_service_spec.rb
index 54c4315d242..52b0ed3412d 100644
--- a/spec/services/projects/blame_service_spec.rb
+++ b/spec/services/projects/blame_service_spec.rb
@@ -54,6 +54,12 @@ RSpec.describe Projects::BlameService, :aggregate_failures do
it { is_expected.to eq(1..2) }
end
+ context 'when user disabled the pagination' do
+ let(:params) { super().merge(no_pagination: 1) }
+
+ it { is_expected.to be_nil }
+ end
+
context 'when feature flag disabled' do
before do
stub_feature_flags(blame_page_pagination: false)
@@ -75,6 +81,12 @@ RSpec.describe Projects::BlameService, :aggregate_failures do
expect(subject.total_count).to eq(4)
end
+ context 'when user disabled the pagination' do
+ let(:params) { super().merge(no_pagination: 1) }
+
+ it { is_expected.to be_nil }
+ end
+
context 'when feature flag disabled' do
before do
stub_feature_flags(blame_page_pagination: false)
diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
index 79904e2bf72..2008de195ab 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -5,11 +5,13 @@ require 'spec_helper'
RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_redis_cache do
using RSpec::Parameterized::TableSyntax
+ include_context 'for a cleanup tags service'
+
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :private) }
let(:repository) { create(:container_repository, :root, project: project) }
- let(:service) { described_class.new(repository, user, params) }
+ let(:service) { described_class.new(container_repository: repository, current_user: user, params: params) }
let(:tags) { %w[latest A Ba Bb C D E] }
before do
@@ -39,268 +41,141 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
describe '#execute' do
subject { service.execute }
- context 'when no params are specified' do
- let(:params) { {} }
-
- it 'does not remove anything' do
- expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
- .not_to receive(:execute)
- expect_no_caching
-
- is_expected.to eq(expected_service_response(before_truncate_size: 0, after_truncate_size: 0, before_delete_size: 0))
- end
- end
-
- context 'when regex matching everything is specified' do
- shared_examples 'removes all matches' do
- it 'does remove all tags except latest' do
- expect_no_caching
-
- expect_delete(%w(A Ba Bb C D E))
-
- is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C D E)))
- end
- end
-
- let(:params) do
- { 'name_regex_delete' => '.*' }
- end
-
- it_behaves_like 'removes all matches'
-
- context 'with deprecated name_regex param' do
- let(:params) do
- { 'name_regex' => '.*' }
- end
-
- it_behaves_like 'removes all matches'
- end
- end
-
- context 'with invalid regular expressions' do
- shared_examples 'handling an invalid regex' do
- it 'keeps all tags' do
- expect_no_caching
-
- expect(Projects::ContainerRepository::DeleteTagsService)
- .not_to receive(:new)
-
- subject
- end
-
- it { is_expected.to eq(status: :error, message: 'invalid regex') }
-
- it 'calls error tracking service' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
-
- subject
- end
- end
-
- context 'when name_regex_delete is invalid' do
- let(:params) { { 'name_regex_delete' => '*test*' } }
-
- it_behaves_like 'handling an invalid regex'
- end
-
- context 'when name_regex is invalid' do
- let(:params) { { 'name_regex' => '*test*' } }
-
- it_behaves_like 'handling an invalid regex'
- end
-
- context 'when name_regex_keep is invalid' do
- let(:params) { { 'name_regex_keep' => '*test*' } }
-
- it_behaves_like 'handling an invalid regex'
- end
- end
-
- context 'when delete regex matching specific tags is used' do
- let(:params) do
- { 'name_regex_delete' => 'C|D' }
- end
-
- it 'does remove C and D' do
- expect_delete(%w(C D))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(C D), before_truncate_size: 2, after_truncate_size: 2, before_delete_size: 2))
- end
-
- context 'with overriding allow regex' do
- let(:params) do
- { 'name_regex_delete' => 'C|D',
- 'name_regex_keep' => 'C' }
- end
-
- it 'does not remove C' do
- expect_delete(%w(D))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
- end
- end
-
- context 'with name_regex_delete overriding deprecated name_regex' do
- let(:params) do
- { 'name_regex' => 'C|D',
- 'name_regex_delete' => 'D' }
- end
-
- it 'does not remove C' do
- expect_delete(%w(D))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
- end
- end
- end
-
- context 'with allow regex value' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'name_regex_keep' => 'B.*' }
- end
-
- it 'does not remove B*' do
- expect_delete(%w(A C D E))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(A C D E), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
- end
- end
-
- context 'when keeping only N tags' do
- let(:params) do
- { 'name_regex' => 'A|B.*|C',
- 'keep_n' => 1 }
- end
-
- it 'sorts tags by date' do
- expect_delete(%w(Bb Ba C))
-
- expect_no_caching
-
- expect(service).to receive(:order_by_date).and_call_original
-
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 3))
- end
- end
-
- context 'when not keeping N tags' do
- let(:params) do
- { 'name_regex' => 'A|B.*|C' }
- end
-
- it 'does not sort tags by date' do
- expect_delete(%w(A Ba Bb C))
-
- expect_no_caching
-
- expect(service).not_to receive(:order_by_date)
-
- is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
- end
- end
-
- context 'when removing keeping only 3' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 3 }
- end
-
- it 'does remove B* and C as they are the oldest' do
- expect_delete(%w(Bb Ba C))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
- end
- end
-
- context 'when removing older than 1 day' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'older_than' => '1 day' }
- end
-
- it 'does remove B* and C as they are older than 1 day' do
- expect_delete(%w(Ba Bb C))
-
- expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(Ba Bb C), before_delete_size: 3))
- end
- end
-
- context 'when combining all parameters' do
+ it_behaves_like 'handling invalid params',
+ service_response_extra: {
+ before_truncate_size: 0,
+ after_truncate_size: 0,
+ before_delete_size: 0,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w(A Ba Bb C D E)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 6,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when delete regex matching specific tags is used',
+ service_response_extra: {
+ before_truncate_size: 2,
+ after_truncate_size: 2,
+ before_delete_size: 2,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex',
+ service_response_extra: {
+ before_truncate_size: 1,
+ after_truncate_size: 1,
+ before_delete_size: 1,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'with allow regex value',
+ delete_expectations: [%w(A C D E)],
+ service_response_extra: {
+ before_truncate_size: 4,
+ after_truncate_size: 4,
+ before_delete_size: 4,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when keeping only N tags',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 4,
+ after_truncate_size: 4,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when not keeping N tags',
+ delete_expectations: [%w(A Ba Bb C)],
+ service_response_extra: {
+ before_truncate_size: 4,
+ after_truncate_size: 4,
+ before_delete_size: 4,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when removing keeping only 3',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when removing older than 1 day',
+ delete_expectations: [%w(Ba Bb C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when combining all parameters',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ it_behaves_like 'when running a container_expiration_policy',
+ delete_expectations: [%w(Bb Ba C)],
+ service_response_extra: {
+ before_truncate_size: 6,
+ after_truncate_size: 6,
+ before_delete_size: 3,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
+ context 'when running a container_expiration_policy with caching' do
+ let(:user) { nil }
let(:params) do
- { 'name_regex_delete' => '.*',
+ {
+ 'name_regex_delete' => '.*',
'keep_n' => 1,
- 'older_than' => '1 day' }
+ 'older_than' => '1 day',
+ 'container_expiration_policy' => true
+ }
end
- it 'does remove B* and C' do
- expect_delete(%w(Bb Ba C))
-
- expect_no_caching
+ it 'expects caching to be used' do
+ expect_delete(%w(Bb Ba C), container_expiration_policy: true)
+ expect_caching
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
+ subject
end
- end
-
- context 'when running a container_expiration_policy' do
- let(:user) { nil }
-
- context 'with valid container_expiration_policy param' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 1,
- 'older_than' => '1 day',
- 'container_expiration_policy' => true }
- end
+ context 'when setting set to false' do
before do
- expect_delete(%w(Bb Ba C), container_expiration_policy: true)
- end
-
- it { is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3)) }
-
- context 'caching' do
- it 'expects caching to be used' do
- expect_caching
-
- subject
- end
-
- context 'when setting set to false' do
- before do
- stub_application_setting(container_registry_expiration_policies_caching: false)
- end
-
- it 'does not use caching' do
- expect_no_caching
-
- subject
- end
- end
+ stub_application_setting(container_registry_expiration_policies_caching: false)
end
- end
- context 'without container_expiration_policy param' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 1,
- 'older_than' => '1 day' }
- end
+ it 'does not use caching' do
+ expect_delete(%w(Bb Ba C), container_expiration_policy: true)
+ expect_no_caching
- it 'fails' do
- is_expected.to eq(status: :error, message: 'access denied')
+ subject
end
end
end
@@ -322,10 +197,12 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
service_response = expected_service_response(
status: status,
original_size: original_size,
+ deleted: nil
+ ).merge(
before_truncate_size: before_truncate_size,
after_truncate_size: after_truncate_size,
before_delete_size: before_delete_size,
- deleted: nil
+ cached_tags_count: 0
)
expect(result).to eq(service_response)
@@ -395,11 +272,8 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
end
it 'caches the created_at values' do
- ::Gitlab::Redis::Cache.with do |redis|
- expect_mget(redis, tags_and_created_ats.keys)
-
- expect_set(redis, cacheable_tags)
- end
+ expect_mget(tags_and_created_ats.keys)
+ expect_set(cacheable_tags)
expect(subject).to include(cached_tags_count: 0)
end
@@ -412,12 +286,10 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
end
it 'uses them' do
- ::Gitlab::Redis::Cache.with do |redis|
- expect_mget(redis, tags_and_created_ats.keys)
+ expect_mget(tags_and_created_ats.keys)
- # because C is already in cache, it should not be cached again
- expect_set(redis, cacheable_tags.except('C'))
- end
+ # because C is already in cache, it should not be cached again
+ expect_set(cacheable_tags.except('C'))
# We will ping the container registry for all tags *except* for C because it's cached
expect(ContainerRegistry::Blob).to receive(:new).with(repository, { "digest" => "sha256:configA" }).and_call_original
@@ -429,15 +301,27 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
end
end
- def expect_mget(redis, keys)
- expect(redis).to receive(:mget).with(keys.map(&method(:cache_key))).and_call_original
+ def expect_mget(keys)
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:mget).with(keys.map(&method(:cache_key))).and_call_original
+ end
end
- def expect_set(redis, tags)
- tags.each do |tag_name, created_at|
+ def expect_set(tags)
+ selected_tags = tags.map do |tag_name, created_at|
ex = 1.day.seconds - (Time.zone.now - created_at).seconds
- if ex > 0
- expect(redis).to receive(:set).with(cache_key(tag_name), rfc3339(created_at), ex: ex.to_i)
+ [tag_name, created_at, ex.to_i] if ex.positive?
+ end.compact
+
+ return if selected_tags.count.zero?
+
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:pipelined).and_call_original
+
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ selected_tags.each do |tag_name, created_at, ex|
+ expect(pipeline).to receive(:set).with(cache_key(tag_name), rfc3339(created_at), ex: ex)
+ end
end
end
end
@@ -476,38 +360,14 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
end
end
- def expect_delete(tags, container_expiration_policy: nil)
- expect(Projects::ContainerRepository::DeleteTagsService)
- .to receive(:new)
- .with(repository.project, user, tags: tags, container_expiration_policy: container_expiration_policy)
- .and_call_original
-
- expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
- .to receive(:execute)
- .with(repository) { { status: :success, deleted: tags } }
- end
-
- # all those -1 because the default tags on L13 have a "latest" that will be filtered out
- def expected_service_response(status: :success, deleted: [], original_size: tags.size, before_truncate_size: tags.size - 1, after_truncate_size: tags.size - 1, before_delete_size: tags.size - 1)
- {
- status: status,
- deleted: deleted,
- original_size: original_size,
- before_truncate_size: before_truncate_size,
- after_truncate_size: after_truncate_size,
- before_delete_size: before_delete_size,
- cached_tags_count: 0
- }.compact.merge(deleted_size: deleted&.size)
- end
-
- def expect_no_caching
- expect(::Gitlab::Redis::Cache).not_to receive(:with)
- end
-
def expect_caching
::Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:mget).and_call_original
- expect(redis).to receive(:set).and_call_original
+ expect(redis).to receive(:pipelined).and_call_original
+
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(:set).and_call_original
+ end
end
end
end
diff --git a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
new file mode 100644
index 00000000000..d2cdb667659
--- /dev/null
+++ b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
+ using RSpec::Parameterized::TableSyntax
+
+ include_context 'for a cleanup tags service'
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :private) }
+
+ let(:repository) { create(:container_repository, :root, :import_done, project: project) }
+ let(:service) { described_class.new(container_repository: repository, current_user: user, params: params) }
+ let(:tags) { %w[latest A Ba Bb C D E] }
+
+ before do
+ project.add_maintainer(user) if user
+
+ stub_container_registry_config(enabled: true)
+
+ stub_const("#{described_class}::TAGS_PAGE_SIZE", tags_page_size)
+
+ one_hour_ago = 1.hour.ago
+ five_days_ago = 5.days.ago
+ six_days_ago = 6.days.ago
+ one_month_ago = 1.month.ago
+
+ stub_tags(
+ {
+ 'latest' => one_hour_ago,
+ 'A' => one_hour_ago,
+ 'Ba' => five_days_ago,
+ 'Bb' => six_days_ago,
+ 'C' => one_month_ago,
+ 'D' => nil,
+ 'E' => nil
+ }
+ )
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'with several tags pages' do
+ let(:tags_page_size) { 2 }
+
+ it_behaves_like 'handling invalid params'
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[E]]
+
+ it_behaves_like 'when delete regex matching specific tags is used'
+
+ it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
+
+ it_behaves_like 'with allow regex value',
+ delete_expectations: [%w[A], %w[C D], %w[E]]
+
+ it_behaves_like 'when keeping only N tags',
+ delete_expectations: [%w[Bb]]
+
+ it_behaves_like 'when not keeping N tags',
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C]]
+
+ context 'when removing keeping only 3' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 3
+ }
+ end
+
+ it_behaves_like 'not removing anything'
+ end
+
+ it_behaves_like 'when removing older than 1 day',
+ delete_expectations: [%w[Ba Bb], %w[C]]
+
+ it_behaves_like 'when combining all parameters',
+ delete_expectations: [%w[Bb], %w[C]]
+
+ it_behaves_like 'when running a container_expiration_policy',
+ delete_expectations: [%w[Bb], %w[C]]
+
+ context 'with a timeout' do
+ let(:params) do
+ { 'name_regex_delete' => '.*' }
+ end
+
+ it 'removes the first few pages' do
+ expect(service).to receive(:timeout?).and_return(false, true)
+
+ expect_delete(%w[A])
+ expect_delete(%w[Ba Bb])
+
+ response = expected_service_response(status: :error, deleted: %w[A Ba Bb], original_size: 4)
+
+ is_expected.to eq(response)
+ end
+ end
+ end
+
+ context 'with a single tags page' do
+ let(:tags_page_size) { 1000 }
+
+ it_behaves_like 'handling invalid params'
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w[A Ba Bb C D E]]
+
+ it_behaves_like 'when delete regex matching specific tags is used'
+
+ it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
+
+ it_behaves_like 'with allow regex value',
+ delete_expectations: [%w[A C D E]]
+
+ it_behaves_like 'when keeping only N tags',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when not keeping N tags',
+ delete_expectations: [%w[A Ba Bb C]]
+
+ it_behaves_like 'when removing keeping only 3',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when removing older than 1 day',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when combining all parameters',
+ delete_expectations: [%w[Ba Bb C]]
+
+ it_behaves_like 'when running a container_expiration_policy',
+ delete_expectations: [%w[Ba Bb C]]
+ end
+ end
+
+ private
+
+ def stub_tags(tags)
+ chunked = tags_page_size < tags.size
+ previous_last = nil
+ max_chunk_index = tags.size / tags_page_size
+
+ tags.keys.in_groups_of(tags_page_size, false).each_with_index do |chunked_tag_names, index|
+ last = index == max_chunk_index
+ pagination_needed = chunked && !last
+
+ response = {
+ pagination: pagination_needed ? pagination_with(last: chunked_tag_names.last) : {},
+ response_body: chunked_tag_names.map do |name|
+ tag_raw_response(name, tags[name])
+ end
+ }
+
+ allow(repository.gitlab_api_client)
+ .to receive(:tags)
+ .with(repository.path, page_size: described_class::TAGS_PAGE_SIZE, last: previous_last)
+ .and_return(response)
+ previous_last = chunked_tag_names.last
+ end
+ end
+
+ def pagination_with(last:)
+ {
+ next: {
+ uri: URI("http://test.org?last=#{last}")
+ }
+ }
+ end
+
+ def tag_raw_response(name, timestamp)
+ timestamp_field = name.start_with?('B') ? 'updated_at' : 'created_at'
+ {
+ 'name' => name,
+ 'digest' => 'sha256:1234567890',
+ 'media_type' => 'application/vnd.oci.image.manifest.v1+json',
+ timestamp_field => timestamp&.iso8601
+ }
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index e112c1e2497..9c8aeb5cf7b 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -125,6 +125,26 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.namespace).to eq(user.namespace)
expect(project.project_namespace).to be_in_sync_with_project(project)
end
+
+ context 'project_authorizations record creation' do
+ context 'when the project_authrizations records are not created via the callback' do
+ it 'still creates project_authrizations record for the user' do
+ # stub out the callback that creates project_authorizations records on the `ProjectMember` model.
+ expect_next_instance_of(ProjectMember) do |member|
+ expect(member).to receive(:refresh_member_authorized_projects).and_return(nil)
+ end
+
+ project = create_project(user, opts)
+
+ expected_record = project.project_authorizations.where(
+ user: user,
+ access_level: ProjectMember::OWNER
+ )
+
+ expect(expected_record).to exist
+ end
+ end
+ end
end
describe 'after create actions' do
@@ -417,10 +437,10 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(imported_project.import_url).to eq('http://import-url')
end
- it 'tracks for the combined_registration experiment', :experiment do
- expect(experiment(:combined_registration)).to track(:import_project).on_next_instance
-
+ it 'tracks for imported project' do
imported_project
+
+ expect_snowplow_event(category: described_class.name, action: 'import_project', user: user)
end
describe 'import scheduling' do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 955384e518c..8269dbebccb 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -135,6 +135,33 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
end
+ context 'deleting a project with merge request diffs' do
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:another_project_mr) { create(:merge_request, source_project: create(:project)) }
+
+ it 'deletes merge request diffs' do
+ merge_request_diffs = merge_request.merge_request_diffs
+ expect(merge_request_diffs.size).to eq(1)
+
+ expect { destroy_project(project, user, {}) }.to change(MergeRequestDiff, :count).by(-1)
+ expect { another_project_mr.reload }.not_to raise_error
+ end
+
+ context 'when extract_mr_diff_deletions feature flag is disabled' do
+ before do
+ stub_feature_flags(extract_mr_diff_deletions: false)
+ end
+
+ it 'also deletes merge request diffs' do
+ merge_request_diffs = merge_request.merge_request_diffs
+ expect(merge_request_diffs.size).to eq(1)
+
+ expect { destroy_project(project, user, {}) }.to change(MergeRequestDiff, :count).by(-1)
+ expect { another_project_mr.reload }.not_to raise_error
+ end
+ end
+ end
+
it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do
@@ -312,7 +339,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
before do
stub_container_registry_tags(repository: project.full_path + '/image',
- tags: ['tag'])
+ tags: ['tag'])
project.container_repositories << container_repository
end
@@ -350,7 +377,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when there are tags for legacy root repository' do
before do
stub_container_registry_tags(repository: project.full_path,
- tags: ['tag'])
+ tags: ['tag'])
end
context 'when image repository tags deletion succeeds' do
@@ -423,11 +450,11 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
destroy_project(project, user)
end
- it 'calls the bulk snippet destroy service with the hard_delete param set to true' do
+ it 'calls the bulk snippet destroy service with the skip_authorization param set to true' do
expect(project.snippets.count).to eq 2
expect_next_instance_of(Snippets::BulkDestroyService, user, project.snippets) do |instance|
- expect(instance).to receive(:execute).with(hard_delete: true).and_call_original
+ expect(instance).to receive(:execute).with(skip_authorization: true).and_call_original
end
expect do
@@ -485,9 +512,11 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
let!(:project_bot) { create(:user, :project_bot).tap { |user| project.add_maintainer(user) } }
it 'deletes bot user as well' do
- expect do
- destroy_project(project, user)
- end.to change { User.find_by(id: project_bot.id) }.to(nil)
+ expect_next_instance_of(Users::DestroyService, user) do |instance|
+ expect(instance).to receive(:execute).with(project_bot, skip_authorization: true).and_call_original
+ end
+
+ destroy_project(project, user)
end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 24b5e35e422..eea2ea3271f 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -19,6 +19,25 @@ RSpec.describe Projects::UpdatePagesService do
subject { described_class.new(project, build) }
+ context 'when a deploy stage already exists' do
+ let!(:stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
+
+ it 'assigns the deploy stage' do
+ subject.execute
+
+ expect(GenericCommitStatus.last.ci_stage).to eq(stage)
+ expect(GenericCommitStatus.last.ci_stage.name).to eq('deploy')
+ end
+ end
+
+ context 'when a deploy stage does not exists' do
+ it 'assigns the deploy stage' do
+ subject.execute
+
+ expect(GenericCommitStatus.last.ci_stage.name).to eq('deploy')
+ end
+ end
+
context 'for new artifacts' do
context "for a valid job" do
let!(:artifacts_archive) { create(:ci_job_artifact, :correct_checksum, file: file, job: build) }
diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb
index 4fa7553c23d..00d1e8b5457 100644
--- a/spec/services/protected_branches/cache_service_spec.rb
+++ b/spec/services/protected_branches/cache_service_spec.rb
@@ -75,10 +75,12 @@ RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do
expect(service.fetch('main', dry_run: true) { true }).to eq(true)
expect(Gitlab::AppLogger).to receive(:error).with(
- 'class' => described_class.name,
- 'message' => /Cache mismatch/,
- 'project_id' => project.id,
- 'project_path' => project.full_path
+ {
+ 'class' => described_class.name,
+ 'message' => /Cache mismatch/,
+ 'project_id' => project.id,
+ 'project_path' => project.full_path
+ }
)
expect(service.fetch('main', dry_run: true) { false }).to eq(false)
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
index 9fa07820148..123deeea005 100644
--- a/spec/services/protected_branches/destroy_service_spec.rb
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProtectedBranches::DestroyService do
let_it_be_with_reload(:project) { create(:project) }
- let(:protected_branch) { create(:protected_branch, project: project) }
+ let!(:protected_branch) { create(:protected_branch, project: project) }
let(:user) { project.first_owner }
subject(:service) { described_class.new(project, user) }
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index c4fe4d78070..2ff6c3c489a 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProtectedBranches::UpdateService do
let_it_be_with_reload(:project) { create(:project) }
- let(:protected_branch) { create(:protected_branch, project: project) }
+ let!(:protected_branch) { create(:protected_branch, project: project) }
let(:user) { project.first_owner }
let(:params) { { name: new_name } }
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 2d38d968ce4..a43f3bc55bf 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1393,14 +1393,41 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ # /draft is a toggle (ff disabled)
it_behaves_like 'draft command' do
let(:content) { '/draft' }
let(:issuable) { merge_request }
+
+ before do
+ stub_feature_flags(draft_quick_action_non_toggle: false)
+ end
end
+ # /draft is a toggle (ff disabled)
it_behaves_like 'ready command' do
let(:content) { '/draft' }
let(:issuable) { merge_request }
+
+ before do
+ stub_feature_flags(draft_quick_action_non_toggle: false)
+ issuable.update!(title: issuable.draft_title)
+ end
+ end
+
+ # /draft is one way (ff enabled)
+ it_behaves_like 'draft command' do
+ let(:content) { '/draft' }
+ let(:issuable) { merge_request }
+ end
+
+ # /draft is one way (ff enabled)
+ it_behaves_like 'draft/ready command no action' do
+ let(:content) { '/draft' }
+ let(:issuable) { merge_request }
+
+ before do
+ issuable.update!(title: issuable.draft_title)
+ end
end
it_behaves_like 'draft/ready command no action' do
@@ -2646,7 +2673,28 @@ RSpec.describe QuickActions::InterpretService do
end
end
- describe 'draft command' do
+ describe 'draft command toggle (deprecated)' do
+ let(:content) { '/draft' }
+
+ before do
+ stub_feature_flags(draft_quick_action_non_toggle: false)
+ end
+
+ it 'includes the new status' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to match_array(['Marks this merge request as a draft.'])
+ end
+
+ it 'sets the ready status on a draft' do
+ merge_request.update!(title: merge_request.draft_title)
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to match_array(["Marks this merge request as ready."])
+ end
+ end
+
+ describe 'draft command set' do
let(:content) { '/draft' }
it 'includes the new status' do
@@ -2654,6 +2702,13 @@ RSpec.describe QuickActions::InterpretService do
expect(explanations).to match_array(['Marks this merge request as a draft.'])
end
+
+ it 'includes the no change message when status unchanged' do
+ merge_request.update!(title: merge_request.draft_title)
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to match_array(["No change to this merge request's draft status."])
+ end
end
describe 'ready command' do
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 2421fab0eec..5f49eed3e77 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -70,6 +70,28 @@ RSpec.describe Releases::CreateService do
expect(result[:release]).not_to be_nil
end
+ context 'and the tag would be protected' do
+ let!(:protected_tag) { create(:protected_tag, project: project, name: tag_name) }
+
+ context 'and the user does not have permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'raises an error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ end
+ end
+
+ context 'and the user has permissions' do
+ it_behaves_like 'a successful release creation'
+ end
+ end
+
context 'and tag_message is provided' do
let(:ref) { 'master' }
let(:tag_name) { 'foobar' }
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index 3d724a79fef..8f89696cc55 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -29,18 +29,35 @@ RSpec.describe ResourceAccessTokens::RevokeService do
expect(resource.reload.users).not_to include(resource_bot)
end
- it 'transfer issuables of bot user to ghost user' do
- issue = create(:issue, author: resource_bot)
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates user removal' do
+ subject
+
+ expect(
+ Users::GhostUserMigration.where(user: resource_bot,
+ initiator_user: user)
+ ).to be_exists
+ end
+ end
- subject
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
- expect(issue.reload.author.ghost?).to be true
- end
+ it 'transfer issuables of bot user to ghost user' do
+ issue = create(:issue, author: resource_bot)
- it 'deletes project bot user' do
- subject
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
+
+ it 'deletes project bot user' do
+ subject
- expect(User.exists?(resource_bot.id)).to be_falsy
+ expect(User.exists?(resource_bot.id)).to be_falsy
+ end
end
it 'logs the event' do
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index 8dc7b07e397..9b0ca54a394 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -98,20 +98,33 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
+ subject(:counter_class) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter }
+
context 'when resource is an issue' do
it 'tracks changed labels' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_label_changed_action)
+ expect(counter_class).to receive(:track_issue_label_changed_action)
change_labels
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL_CHANGED }
+ let(:user) { author }
+ subject(:service_action) { change_labels }
+ end
end
context 'when resource is a merge request' do
let(:resource) { create(:merge_request, source_project: project) }
it 'does not track changed labels' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
- .not_to receive(:track_issue_label_changed_action)
+ expect(counter_class).not_to receive(:track_issue_label_changed_action)
+
+ change_labels
+ end
+
+ it 'does not emit snowplow event', :snowplow do
+ expect_no_snowplow_event
change_labels
end
diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
index 1fd196cdcee..7a004e2915c 100644
--- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Security::CiConfiguration::SastParserService do
- include Ci::TemplateHelpers
-
describe '#configuration' do
include_context 'read ci configuration for sast enabled project'
diff --git a/spec/services/service_ping/submit_service_ping_service_spec.rb b/spec/services/service_ping/submit_service_ping_service_spec.rb
index b863b2a46b0..5dbf5edb776 100644
--- a/spec/services/service_ping/submit_service_ping_service_spec.rb
+++ b/spec/services/service_ping/submit_service_ping_service_spec.rb
@@ -54,11 +54,13 @@ RSpec.describe ServicePing::SubmitService do
let(:service_ping_payload_url) { File.join(described_class::STAGING_BASE_URL, described_class::USAGE_DATA_PATH) }
let(:service_ping_errors_url) { File.join(described_class::STAGING_BASE_URL, described_class::ERROR_PATH) }
let(:service_ping_metadata_url) { File.join(described_class::STAGING_BASE_URL, described_class::METADATA_PATH) }
+ let!(:usage_data) { { uuid: 'uuid', recorded_at: Time.current } }
+
+ let(:subject) { described_class.new(payload: usage_data) }
shared_examples 'does not run' do
it do
expect(Gitlab::HTTP).not_to receive(:post)
- expect(Gitlab::Usage::ServicePingReport).not_to receive(:for)
subject.execute
end
@@ -69,7 +71,7 @@ RSpec.describe ServicePing::SubmitService do
expect(Gitlab::HTTP).not_to receive(:post).with(service_ping_payload_url, any_args)
expect { subject.execute }.to raise_error(described_class::SubmissionError) do |error|
- expect(error.message).to include('Usage data is blank')
+ expect(error.message).to include('Usage data payload is blank')
end
end
end
@@ -118,13 +120,18 @@ RSpec.describe ServicePing::SubmitService do
allow(ServicePing::ServicePingSettings).to receive(:product_intelligence_enabled?).and_return(true)
end
- it 'generates service ping' do
- stub_response(body: with_dev_ops_score_params)
- stub_response(body: nil, url: service_ping_metadata_url, status: 201)
+ it 'submits a service ping payload without errors', :aggregate_failures do
+ response = stub_response(body: with_dev_ops_score_params)
+ error_response = stub_response(body: nil, url: service_ping_errors_url, status: 201)
+ metadata_response = stub_response(body: nil, url: service_ping_metadata_url, status: 201)
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_call_original
+ expect(Gitlab::HTTP).to receive(:post).twice.and_call_original
subject.execute
+
+ expect(response).to have_been_requested
+ expect(error_response).not_to have_been_requested
+ expect(metadata_response).to have_been_requested
end
end
@@ -155,15 +162,9 @@ RSpec.describe ServicePing::SubmitService do
expect(response).to have_been_requested
end
- it 'forces a refresh of usage data statistics before submitting' do
- stub_response(body: with_dev_ops_score_params)
-
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_call_original
-
- subject.execute
- end
-
context 'when conv_index data is passed' do
+ let(:usage_data) { { uuid: 'uuid', recorded_at: Time.current } }
+
before do
stub_response(body: with_conv_index_params)
end
@@ -171,21 +172,17 @@ RSpec.describe ServicePing::SubmitService do
it_behaves_like 'saves DevOps report data from the response'
it 'saves usage_data_id to version_usage_data_id_value' do
- recorded_at = Time.current
- usage_data = { uuid: 'uuid', recorded_at: recorded_at }
-
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values)
- .and_return(usage_data)
-
subject.execute
- raw_usage_data = RawUsageData.find_by(recorded_at: recorded_at)
+ raw_usage_data = RawUsageData.find_by(recorded_at: usage_data[:recorded_at])
expect(raw_usage_data.version_usage_data_id_value).to eq(31643)
end
end
context 'when only usage_data_id is passed in response' do
+ let(:usage_data) { { uuid: 'uuid', recorded_at: Time.current } }
+
before do
stub_response(body: with_usage_data_id_params)
end
@@ -195,15 +192,9 @@ RSpec.describe ServicePing::SubmitService do
end
it 'saves usage_data_id to version_usage_data_id_value' do
- recorded_at = Time.current
- usage_data = { uuid: 'uuid', recorded_at: recorded_at }
-
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values)
- .and_return(usage_data)
-
subject.execute
- raw_usage_data = RawUsageData.find_by(recorded_at: recorded_at)
+ raw_usage_data = RawUsageData.find_by(recorded_at: usage_data[:recorded_at])
expect(raw_usage_data.version_usage_data_id_value).to eq(31643)
end
@@ -232,6 +223,8 @@ RSpec.describe ServicePing::SubmitService do
end
context 'with saving raw_usage_data' do
+ let(:usage_data) { { uuid: 'uuid', recorded_at: Time.current } }
+
before do
stub_response(body: with_dev_ops_score_params)
end
@@ -241,17 +234,10 @@ RSpec.describe ServicePing::SubmitService do
end
it 'saves the correct payload' do
- recorded_at = Time.current
- usage_data = { uuid: 'uuid', recorded_at: recorded_at }
-
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values)
- .and_return(usage_data)
-
subject.execute
- raw_usage_data = RawUsageData.find_by(recorded_at: recorded_at)
+ raw_usage_data = RawUsageData.find_by(recorded_at: usage_data[:recorded_at])
- expect(raw_usage_data.recorded_at).to be_like_time(recorded_at)
expect(raw_usage_data.payload.to_json).to eq(usage_data.to_json)
end
end
@@ -269,90 +255,30 @@ RSpec.describe ServicePing::SubmitService do
end
context 'and usage data is empty string' do
- before do
- allow(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return({})
- end
+ let(:usage_data) { {} }
it_behaves_like 'does not send a blank usage ping payload'
end
context 'and usage data is nil' do
- before do
- allow(ServicePing::BuildPayload).to receive(:execute).and_return(nil)
- allow(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values).and_return(nil)
- end
+ let(:usage_data) { nil }
it_behaves_like 'does not send a blank usage ping payload'
end
- context 'if payload service fails' do
- before do
- stub_response(body: with_dev_ops_score_params)
-
- allow(ServicePing::BuildPayload).to receive_message_chain(:new, :execute)
- .and_raise(described_class::SubmissionError, 'SubmissionError')
- end
-
- it 'calls Gitlab::Usage::ServicePingReport .for method' do
- usage_data = build_usage_data
-
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values)
- .and_return(usage_data)
-
- subject.execute
- end
-
- it 'submits error' do
- expect(Gitlab::HTTP).to receive(:post).with(URI.join(service_ping_payload_url), any_args)
- .and_call_original
- expect(Gitlab::HTTP).to receive(:post).with(URI.join(service_ping_errors_url), any_args)
- .and_call_original
- expect(Gitlab::HTTP).to receive(:post).with(URI.join(service_ping_metadata_url), any_args)
- .and_call_original
-
- subject.execute
- end
- end
-
- context 'calls BuildPayload first' do
- before do
- stub_response(body: with_dev_ops_score_params)
- end
-
- it 'returns usage data' do
- usage_data = build_usage_data
-
- expect_next_instance_of(ServicePing::BuildPayload) do |service|
- expect(service).to receive(:execute).and_return(usage_data)
- end
-
- subject.execute
- end
- end
-
context 'if version app response fails' do
before do
stub_response(body: with_dev_ops_score_params, status: 404)
-
- usage_data = build_usage_data
- allow_next_instance_of(ServicePing::BuildPayload) do |service|
- allow(service).to receive(:execute).and_return(usage_data)
- end
end
- it 'calls Gitlab::Usage::ServicePingReport .for method' do
- usage_data = build_usage_data
-
- expect(Gitlab::Usage::ServicePingReport).to receive(:for).with(output: :all_metrics_values)
- .and_return(usage_data)
-
+ it 'raises SubmissionError' do
# SubmissionError is raised as a result of 404 in response from HTTP Request
expect { subject.execute }.to raise_error(described_class::SubmissionError)
end
end
context 'when skip_db_write passed to service' do
- let(:subject) { ServicePing::SubmitService.new(skip_db_write: true) }
+ let(:subject) { described_class.new(payload: usage_data, skip_db_write: true) }
before do
stub_response(body: with_dev_ops_score_params)
@@ -377,21 +303,18 @@ RSpec.describe ServicePing::SubmitService do
stub_database_flavor_check
stub_application_setting(usage_ping_enabled: true)
stub_response(body: with_conv_index_params)
- allow_next_instance_of(ServicePing::BuildPayload) do |service|
- allow(service).to receive(:execute).and_return(payload)
- end
end
let(:metric_double) { instance_double(Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator, duration: 123) }
- let(:payload) do
+ let(:usage_data) do
{
uuid: 'uuid',
- metric_a: metric_double,
- metric_group: {
+ metric_a: metric_double,
+ metric_group: {
metric_b: metric_double
},
- metric_without_timing: "value",
- recorded_at: Time.current
+ metric_without_timing: "value",
+ recorded_at: Time.current
}
end
@@ -399,10 +322,10 @@ RSpec.describe ServicePing::SubmitService do
{
metadata: {
uuid: 'uuid',
- metrics: [
- { name: 'metric_a', time_elapsed: 123 },
- { name: 'metric_group.metric_b', time_elapsed: 123 }
- ]
+ metrics: [
+ { name: 'metric_a', time_elapsed: 123 },
+ { name: 'metric_group.metric_b', time_elapsed: 123 }
+ ]
}
}
end
@@ -425,8 +348,4 @@ RSpec.describe ServicePing::SubmitService do
status: status
)
end
-
- def build_usage_data
- { uuid: 'uuid', recorded_at: Time.current }
- end
end
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index 3ede90fbc44..2d70979dd3a 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -43,14 +43,14 @@ RSpec.describe ServiceResponse do
end
describe '.error' do
- it 'creates a failed response without HTTP status' do
+ it 'creates an error response without HTTP status' do
response = described_class.error(message: 'Bad apple')
expect(response).to be_error
expect(response.message).to eq('Bad apple')
end
- it 'creates a failed response with HTTP status' do
+ it 'creates an error response with HTTP status' do
response = described_class.error(message: 'Bad apple', http_status: 400)
expect(response).to be_error
@@ -58,7 +58,7 @@ RSpec.describe ServiceResponse do
expect(response.http_status).to eq(400)
end
- it 'creates a failed response with payload' do
+ it 'creates an error response with payload' do
response = described_class.error(message: 'Bad apple',
payload: { bad: 'apple' })
@@ -66,6 +66,15 @@ RSpec.describe ServiceResponse do
expect(response.message).to eq('Bad apple')
expect(response.payload).to eq(bad: 'apple')
end
+
+ it 'creates an error response with a reason' do
+ response = described_class.error(message: 'Bad apple',
+ reason: :permission_denied)
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ expect(response.reason).to eq(:permission_denied)
+ end
end
describe '#success?' do
diff --git a/spec/services/snippets/bulk_destroy_service_spec.rb b/spec/services/snippets/bulk_destroy_service_spec.rb
index 2d2bdd116d1..4142aa349e4 100644
--- a/spec/services/snippets/bulk_destroy_service_spec.rb
+++ b/spec/services/snippets/bulk_destroy_service_spec.rb
@@ -71,8 +71,8 @@ RSpec.describe Snippets::BulkDestroyService do
let(:error_message) { "You don't have access to delete these snippets." }
end
- context 'when hard_delete option is passed' do
- subject { described_class.new(service_user, snippets).execute(hard_delete: true) }
+ context 'when skip_authorization option is passed' do
+ subject { described_class.new(service_user, snippets).execute(skip_authorization: true) }
it 'returns a ServiceResponse success response' do
expect(subject).to be_success
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
index bd8418d7092..4dfec9735ba 100644
--- a/spec/services/spam/spam_action_service_spec.rb
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Spam::SpamActionService do
include_context 'includes Spam constants'
let(:issue) { create(:issue, project: project, author: author) }
+ let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
+ let(:project_snippet) { create(:project_snippet, :public, author: author) }
let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referer) { 'fake-http-referer' }
@@ -27,6 +29,7 @@ RSpec.describe Spam::SpamActionService do
before do
issue.spam = false
+ personal_snippet.spam = false
end
describe 'constructor argument validation' do
@@ -50,24 +53,24 @@ RSpec.describe Spam::SpamActionService do
end
end
- shared_examples 'creates a spam log' do
+ shared_examples 'creates a spam log' do |target_type|
it do
expect { subject }
- .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
+ .to log_spam(title: target.title, description: target.description, noteable_type: target_type)
# TODO: These checks should be incorporated into the `log_spam` RSpec matcher above
new_spam_log = SpamLog.last
expect(new_spam_log.user_id).to eq(user.id)
- expect(new_spam_log.title).to eq(issue.title)
- expect(new_spam_log.description).to eq(issue.description)
+ expect(new_spam_log.title).to eq(target.title)
+ expect(new_spam_log.description).to eq(target.spam_description)
expect(new_spam_log.source_ip).to eq(fake_ip)
expect(new_spam_log.user_agent).to eq(fake_user_agent)
- expect(new_spam_log.noteable_type).to eq('Issue')
+ expect(new_spam_log.noteable_type).to eq(target_type)
expect(new_spam_log.via_api).to eq(true)
end
end
- describe '#execute' do
+ shared_examples 'execute spam action service' do |target_type|
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false }
@@ -82,20 +85,22 @@ RSpec.describe Spam::SpamActionService do
let(:verdict_service_args) do
{
- target: issue,
+ target: target,
user: user,
options: verdict_service_opts,
context: {
action: :create,
- target_type: 'Issue'
- }
+ target_type: target_type
+ },
+ extra_features: extra_features
}
end
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do
- described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
+ described_service = described_class.new(spammable: target, spam_params: spam_params, extra_features:
+ extra_features, user: user, action: :create)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
described_service.execute
end
@@ -136,7 +141,7 @@ RSpec.describe Spam::SpamActionService do
context 'when spammable attributes have not changed' do
before do
- issue.closed_at = Time.zone.now
+ allow(target).to receive(:has_changes_to_save?).and_return(true)
end
it 'does not create a spam log' do
@@ -146,11 +151,11 @@ RSpec.describe Spam::SpamActionService do
context 'when spammable attributes have changed' do
let(:expected_service_check_response_message) do
- /Check Issue spammable model for any errors or CAPTCHA requirement/
+ /Check #{target_type} spammable model for any errors or CAPTCHA requirement/
end
before do
- issue.description = 'Lovely Spam! Wonderful Spam!'
+ target.description = 'Lovely Spam! Wonderful Spam!'
end
context 'when allowlisted' do
@@ -170,13 +175,13 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW)
end
- it_behaves_like 'creates a spam log'
+ it_behaves_like 'creates a spam log', target_type
it 'marks as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
- expect(issue).to be_spam
+ expect(target).to be_spam
end
end
@@ -185,13 +190,13 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER)
end
- it_behaves_like 'creates a spam log'
+ it_behaves_like 'creates a spam log', target_type
it 'marks as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
- expect(issue).to be_spam
+ expect(target).to be_spam
end
end
@@ -200,20 +205,20 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)
end
- it_behaves_like 'creates a spam log'
+ it_behaves_like 'creates a spam log', target_type
it 'does not mark as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
- expect(issue).not_to be_spam
+ expect(target).not_to be_spam
end
it 'marks as needing reCAPTCHA' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
- expect(issue).to be_needs_recaptcha
+ expect(target).to be_needs_recaptcha
end
end
@@ -222,20 +227,20 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM)
end
- it_behaves_like 'creates a spam log'
+ it_behaves_like 'creates a spam log', target_type
it 'does not mark as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
- expect(issue).not_to be_spam
+ expect(target).not_to be_spam
end
it 'does not mark as needing CAPTCHA' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
- expect(issue).not_to be_needs_recaptcha
+ expect(target).not_to be_needs_recaptcha
end
end
@@ -249,7 +254,7 @@ RSpec.describe Spam::SpamActionService do
end
it 'clears spam flags' do
- expect(issue).to receive(:clear_spam_flags!)
+ expect(target).to receive(:clear_spam_flags!)
subject
end
@@ -265,7 +270,7 @@ RSpec.describe Spam::SpamActionService do
end
it 'clears spam flags' do
- expect(issue).to receive(:clear_spam_flags!)
+ expect(target).to receive(:clear_spam_flags!)
subject
end
@@ -285,4 +290,27 @@ RSpec.describe Spam::SpamActionService do
end
end
end
+
+ describe '#execute' do
+ describe 'issue' do
+ let(:target) { issue }
+ let(:extra_features) { {} }
+
+ it_behaves_like 'execute spam action service', 'Issue'
+ end
+
+ describe 'project snippet' do
+ let(:target) { project_snippet }
+ let(:extra_features) { { files: [{ path: 'project.rb' }] } }
+
+ it_behaves_like 'execute spam action service', 'ProjectSnippet'
+ end
+
+ describe 'personal snippet' do
+ let(:target) { personal_snippet }
+ let(:extra_features) { { files: [{ path: 'personal.rb' }] } }
+
+ it_behaves_like 'execute spam action service', 'PersonalSnippet'
+ end
+ end
end
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index 082b8f909f9..b89c96129c2 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -17,9 +17,10 @@ RSpec.describe Spam::SpamVerdictService do
let(:check_for_spam) { true }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, author: user) }
+ let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
let(:service) do
- described_class.new(user: user, target: issue, options: {})
+ described_class.new(user: user, target: target, options: {})
end
let(:attribs) do
@@ -31,7 +32,7 @@ RSpec.describe Spam::SpamVerdictService do
stub_feature_flags(allow_possible_spam: false)
end
- describe '#execute' do
+ shared_examples 'execute spam verdict service' do
subject { service.execute }
before do
@@ -172,7 +173,8 @@ RSpec.describe Spam::SpamVerdictService do
end
end
- describe '#akismet_verdict' do
+ shared_examples 'akismet verdict' do
+ let(:target) { issue }
subject { service.send(:akismet_verdict) }
context 'if Akismet is enabled' do
@@ -227,7 +229,7 @@ RSpec.describe Spam::SpamVerdictService do
end
end
- describe '#spamcheck_verdict' do
+ shared_examples 'spamcheck verdict' do
subject { service.send(:spamcheck_verdict) }
context 'if a Spam Check endpoint enabled and set to a URL' do
@@ -254,7 +256,7 @@ RSpec.describe Spam::SpamVerdictService do
before do
allow(service).to receive(:spamcheck_client).and_return(spam_client)
- allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error])
+ allow(spam_client).to receive(:spam?).and_return([verdict, attribs, error])
end
context 'if the result is a NOOP verdict' do
@@ -365,10 +367,13 @@ RSpec.describe Spam::SpamVerdictService do
let(:attribs) { nil }
before do
- allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted)
+ allow(spam_client).to receive(:spam?).and_raise(GRPC::Aborted)
end
it 'returns nil' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ an_instance_of(GRPC::Aborted), error: ::Spam::SpamConstants::ERROR_TYPE
+ )
expect(subject).to eq([ALLOW, attribs, true])
end
end
@@ -381,17 +386,20 @@ RSpec.describe Spam::SpamVerdictService do
expect(subject).to eq [DISALLOW, attribs]
end
end
- end
- context 'if the endpoint times out' do
- let(:attribs) { nil }
+ context 'if the endpoint times out' do
+ let(:attribs) { nil }
- before do
- allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded)
- end
+ before do
+ allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded)
+ end
- it 'returns nil' do
- expect(subject).to eq([ALLOW, attribs, true])
+ it 'returns nil' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ an_instance_of(GRPC::DeadlineExceeded), error: ::Spam::SpamConstants::ERROR_TYPE
+ )
+ expect(subject).to eq([ALLOW, attribs, true])
+ end
end
end
end
@@ -416,4 +424,46 @@ RSpec.describe Spam::SpamVerdictService do
end
end
end
+
+ describe '#execute' do
+ describe 'issue' do
+ let(:target) { issue }
+
+ it_behaves_like 'execute spam verdict service'
+ end
+
+ describe 'snippet' do
+ let(:target) { snippet }
+
+ it_behaves_like 'execute spam verdict service'
+ end
+ end
+
+ describe '#akismet_verdict' do
+ describe 'issue' do
+ let(:target) { issue }
+
+ it_behaves_like 'akismet verdict'
+ end
+
+ describe 'snippet' do
+ let(:target) { snippet }
+
+ it_behaves_like 'akismet verdict'
+ end
+ end
+
+ describe '#spamcheck_verdict' do
+ describe 'issue' do
+ let(:target) { issue }
+
+ it_behaves_like 'spamcheck verdict'
+ end
+
+ describe 'snippet' do
+ let(:target) { snippet }
+
+ it_behaves_like 'spamcheck verdict'
+ end
+ end
end
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index e34324d5fe2..41ccd8523fa 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Suggestions::ApplyService do
def apply(suggestions, custom_message = nil)
result = apply_service.new(user, *suggestions, message: custom_message).execute
- suggestions.map { |suggestion| suggestion.reload }
+ suggestions.map(&:reload)
expect(result[:status]).to eq(:success)
end
@@ -136,21 +136,20 @@ RSpec.describe Suggestions::ApplyService do
end
let(:merge_request) do
- create(:merge_request, source_project: project,
- target_project: project,
- source_branch: 'master')
+ create(:merge_request,
+ source_project: project, target_project: project, source_branch: 'master')
end
let(:position) { build_position }
let(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position, project: project)
+ create(:diff_note_on_merge_request,
+ noteable: merge_request, position: position, project: project)
end
let(:suggestion) do
- create(:suggestion, :content_from_repo, note: diff_note,
- to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n")
+ create(:suggestion, :content_from_repo,
+ note: diff_note, to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n")
end
let(:suggestion2) do
@@ -311,9 +310,9 @@ RSpec.describe Suggestions::ApplyService do
context 'when HEAD from position is different from source branch HEAD on repo' do
it 'returns error message' do
- allow(suggestion).to receive(:appliable?) { true }
- allow(suggestion.position).to receive(:head_sha) { 'old-sha' }
- allow(suggestion.noteable).to receive(:source_branch_sha) { 'new-sha' }
+ allow(suggestion).to receive(:appliable?).and_return(true)
+ allow(suggestion.position).to receive(:head_sha).and_return('old-sha')
+ allow(suggestion.noteable).to receive(:source_branch_sha).and_return('new-sha')
result = apply_service.new(user, suggestion).execute
@@ -430,7 +429,6 @@ RSpec.describe Suggestions::ApplyService do
suggestion1_diff = fetch_raw_diff(suggestion1)
suggestion2_diff = fetch_raw_diff(suggestion2)
- # rubocop: disable Layout/TrailingWhitespace
expected_suggestion1_diff = <<-CONTENT.strip_heredoc
@@ -10,7 +10,7 @@ module Popen
end
@@ -442,9 +440,6 @@ RSpec.describe Suggestions::ApplyService do
"PWD" => path
}
CONTENT
- # rubocop: enable Layout/TrailingWhitespace
-
- # rubocop: disable Layout/TrailingWhitespace
expected_suggestion2_diff = <<-CONTENT.strip_heredoc
@@ -28,7 +28,7 @@ module Popen
@@ -455,8 +450,6 @@ RSpec.describe Suggestions::ApplyService do
@cmd_status = wait_thr.value.exitstatus
end
CONTENT
- # rubocop: enable Layout/TrailingWhitespace
-
expect(suggestion1_diff.strip).to eq(expected_suggestion1_diff.strip)
expect(suggestion2_diff.strip).to eq(expected_suggestion2_diff.strip)
end
@@ -508,10 +501,8 @@ RSpec.describe Suggestions::ApplyService do
end
let(:suggestion) do
- create(:suggestion, :content_from_repo, note: diff_note,
- lines_above: 2,
- lines_below: 3,
- to_content: "# multi\n# line\n")
+ create(:suggestion, :content_from_repo,
+ note: diff_note, lines_above: 2, lines_below: 3, to_content: "# multi\n# line\n")
end
let(:suggestions) { [suggestion] }
@@ -568,7 +559,7 @@ RSpec.describe Suggestions::ApplyService do
end
let(:suggestion) do
- create_suggestion( to_content: "", new_line: 13)
+ create_suggestion(to_content: "", new_line: 13)
end
let(:suggestions) { [suggestion] }
@@ -616,14 +607,12 @@ RSpec.describe Suggestions::ApplyService do
context 'no permission' do
let(:merge_request) do
- create(:merge_request, source_project: project,
- target_project: project)
+ create(:merge_request, source_project: project, target_project: project)
end
let(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position,
- project: project)
+ create(:diff_note_on_merge_request,
+ noteable: merge_request, position: position, project: project)
end
context 'user cannot write in project repo' do
@@ -642,14 +631,12 @@ RSpec.describe Suggestions::ApplyService do
context 'patch is not appliable' do
let(:merge_request) do
- create(:merge_request, source_project: project,
- target_project: project)
+ create(:merge_request, source_project: project, target_project: project)
end
let(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position,
- project: project)
+ create(:diff_note_on_merge_request,
+ noteable: merge_request, position: position, project: project)
end
before do
@@ -669,7 +656,7 @@ RSpec.describe Suggestions::ApplyService do
let(:result) { apply_service.new(user, suggestion).execute }
before do
- expect(suggestion.note).to receive(:latest_diff_file) { nil }
+ expect(suggestion.note).to receive(:latest_diff_file).and_return(nil)
end
it 'returns error message' do
diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb
index 33608deaa64..c856caa3f3e 100644
--- a/spec/services/system_notes/time_tracking_service_spec.rb
+++ b/spec/services/system_notes/time_tracking_service_spec.rb
@@ -48,12 +48,6 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
expect(note.note).to eq("changed due date to #{due_date.to_s(:long)}")
end
- it 'tracks the issue event in usage ping' do
- expect(activity_counter_class).to receive(activity_counter_method).with(author: author)
-
- subject
- end
-
context 'and start date removed' do
let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [start_date, nil] } }
@@ -66,12 +60,18 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
context 'when start_date is added' do
let(:changed_dates) { { 'start_date' => [nil, start_date] } }
- it 'does not track the issue event in usage ping' do
+ it 'does not track the issue event' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action)
subject
end
+ it 'does not emit snowplow event', :snowplow do
+ expect_no_snowplow_event
+
+ subject
+ end
+
it 'sets the correct note message' do
expect(note.note).to eq("changed start date to #{start_date.to_s(:long)}")
end
@@ -111,12 +111,19 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
subject
end
- it 'tracks the issue event in usage ping' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_due_date_changed_action).with(author: author)
+ it 'tracks the issue event' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_due_date_changed_action)
+ .with(author: author, project: project)
subject
end
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_DUE_DATE_CHANGED }
+ let(:user) { author }
+ subject(:service_action) { note }
+ end
+
context 'when only start_date is added' do
let(:changed_dates) { { 'start_date' => [nil, start_date] } }
@@ -135,12 +142,18 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
it_behaves_like 'issuable getting date change notes'
- it 'does not track the issue event in usage ping' do
+ it 'does not track the issue event' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action)
subject
end
+ it 'does not emit snowplow event', :snowplow do
+ expect_no_snowplow_event
+
+ subject
+ end
+
context 'when only start_date is added' do
let(:changed_dates) { { 'start_date' => [nil, start_date] } }
@@ -155,12 +168,23 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
context 'when noteable is a merge request' do
let(:noteable) { create(:merge_request, source_project: project) }
- it 'does not track the issue event in usage ping' do
+ it 'does not track the issue event' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action)
+
+ subject
+ end
+
+ it 'does not track the work item event in usage ping' do
expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_date_changed_action)
subject
end
+
+ it 'does not emit snowplow event', :snowplow do
+ expect_no_snowplow_event
+
+ subject
+ end
end
end
@@ -201,17 +225,31 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
end
it 'tracks the issue event in usage ping' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_time_estimate_changed_action).with(author: author)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_time_estimate_changed_action)
+ .with(author: author, project: project)
subject
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_TIME_ESTIMATE_CHANGED }
+ let(:user) { author }
+ let(:service_action) { subject }
+ end
end
context 'when noteable is a merge request' do
let_it_be(:noteable) { create(:merge_request, source_project: project) }
- it 'does not track the issue event in usage ping' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_time_estimate_changed_action).with(author: author)
+ it 'does not track the issue event' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_time_estimate_changed_action)
+ .with(author: author, project: project)
+
+ subject
+ end
+
+ it 'does not emit snowplow event', :snowplow do
+ expect_no_snowplow_event
subject
end
@@ -316,25 +354,42 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
end
end
- it 'tracks the issue event in usage ping' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_time_spent_changed_action).with(author: author)
+ it 'tracks the issue event' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_time_spent_changed_action)
+ .with(author: author, project: project)
spend_time!(277200)
subject
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_TIME_SPENT_CHANGED }
+ let(:user) { author }
+ let(:service_action) do
+ spend_time!(277200)
+ subject
+ end
+ end
end
context 'when noteable is a merge request' do
let_it_be(:noteable) { create(:merge_request, source_project: project) }
- it 'does not track the issue event in usage ping' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_time_estimate_changed_action).with(author: author)
+ it 'does not track the issue event' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_time_estimate_changed_action)
+ .with(author: author, project: project)
spend_time!(277200)
subject
end
+
+ it 'does not emit snowplow event', :snowplow do
+ expect_no_snowplow_event
+
+ subject
+ end
end
def spend_time!(seconds)
diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb
index 971917eb8e9..eef31817aa8 100644
--- a/spec/services/topics/merge_service_spec.rb
+++ b/spec/services/topics/merge_service_spec.rb
@@ -30,7 +30,9 @@ RSpec.describe Topics::MergeService do
it 'reverts previous changes' do
allow(source_topic.reload).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)
- expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed)
+ response = subject
+ expect(response).to be_error
+ expect(response.message).to eq('Topics could not be merged!')
expect(source_topic.projects).to contain_exactly(project_1, project_2, project_4)
expect(target_topic.projects).to contain_exactly(project_3, project_4)
@@ -50,9 +52,9 @@ RSpec.describe Topics::MergeService do
with_them do
it 'raises correct error' do
- expect { subject }.to raise_error(ArgumentError) do |error|
- expect(error.message).to eq(expected_message)
- end
+ response = subject
+ expect(response).to be_error
+ expect(response.message).to eq(expected_message)
end
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 90c4f70d749..b32599d4af8 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -10,371 +10,475 @@ RSpec.describe Users::DestroyService do
let(:service) { described_class.new(admin) }
let(:gitlab_shell) { Gitlab::Shell.new }
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'deletes the user' do
- user_data = service.execute(user)
-
- expect(user_data['email']).to eq(user.email)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ shared_examples 'pre-migrate clean-up' do
+ describe "Deletes a user and all their personal projects", :enable_admin_mode do
+ context 'no options are given' do
+ it 'will delete the personal project' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
+ end
+
+ service.execute(user)
+ end
end
- it 'deletes user associations in batches' do
- expect(user).to receive(:destroy_dependent_associations_in_batches)
+ context 'personal projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save!
+ end
+
+ it 'destroys a personal project in pending_delete' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
+ end
- service.execute(user)
+ service.execute(user)
+ end
end
- it 'does not include snippets when deleting in batches' do
- expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
+ context "solo owned groups present" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
- service.execute(user)
- end
+ before do
+ solo_owned.group_members = [member]
+ end
- it 'will delete the project' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
+ it 'returns the user with attached errors' do
+ expect(service.execute(user)).to be(user)
+ expect(user.errors.full_messages).to(
+ contain_exactly('You must transfer ownership or delete groups before you can remove user'))
end
- service.execute(user)
+ it 'does not delete the user, nor the group' do
+ service.execute(user)
+
+ expect(User.find(user.id)).to eq user
+ expect(Group.find(solo_owned.id)).to eq solo_owned
+ end
end
- it 'calls the bulk snippet destroy service for the user personal snippets' do
- repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
- repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
+ context "deletions with solo owned groups" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
- aggregate_failures do
- expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_truthy
- expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_truthy
+ before do
+ solo_owned.group_members = [member]
+ service.execute(user, delete_solo_owned_groups: true)
end
- # Call made when destroying user personal projects
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, project.snippets).and_call_original
+ it 'deletes solo owned groups' do
+ expect { Group.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
- # Call to remove user personal snippets and for
- # project snippets where projects are not user personal
- # ones
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, user.snippets.only_personal_snippets).and_call_original
+ context 'deletions with inherited group owners' do
+ let(:group) { create(:group, :nested) }
+ let(:user) { create(:user) }
+ let(:inherited_owner) { create(:user) }
+
+ before do
+ group.parent.add_owner(inherited_owner)
+ group.add_owner(user)
- service.execute(user)
+ service.execute(user, delete_solo_owned_groups: true)
+ end
- aggregate_failures do
- expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_falsey
- expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_falsey
+ it 'does not delete the group' do
+ expect(Group.exists?(id: group)).to be_truthy
end
end
- it 'calls the bulk snippet destroy service with hard delete option if it is present' do
- # this avoids getting into Projects::DestroyService as it would
- # call Snippets::BulkDestroyService first!
- allow(user).to receive(:personal_projects).and_return([])
+ describe "user personal's repository removal" do
+ context 'storages' do
+ before do
+ perform_enqueued_jobs { service.execute(user) }
+ end
- expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service|
- expect(bulk_destroy_service).to receive(:execute).with({ hard_delete: true }).and_call_original
- end
+ context 'legacy storage' do
+ let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
- service.execute(user, { hard_delete: true })
- end
+ it 'removes repository' do
+ expect(
+ gitlab_shell.repository_exists?(project.repository_storage,
+ "#{project.disk_path}.git")
+ ).to be_falsey
+ end
+ end
- it 'does not delete project snippets that the user is the author of' do
- repo = create(:project_snippet, :repository, author: user).snippet_repository
- service.execute(user)
- expect(gitlab_shell.repository_exists?(repo.shard_name, repo.disk_path + '.git')).to be_truthy
- expect(User.ghost.snippets).to include(repo.snippet)
- end
+ context 'hashed storage' do
+ let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
- context 'when an error is raised deleting snippets' do
- it 'does not delete user' do
- snippet = create(:personal_snippet, :repository, author: user)
+ it 'removes repository' do
+ expect(
+ gitlab_shell.repository_exists?(project.repository_storage,
+ "#{project.disk_path}.git")
+ ).to be_falsey
+ end
+ end
+ end
- bulk_service = double
- allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
- allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
- allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+ context 'repository removal status is taken into account' do
+ it 'raises exception' do
+ expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).and_return(false)
+ end
- aggregate_failures do
expect { service.execute(user) }
- .to raise_error(Users::DestroyService::DestroyError, 'foo' )
- expect(snippet.reload).not_to be_nil
- expect(gitlab_shell.repository_exists?(snippet.repository_storage, snippet.disk_path + '.git')).to be_truthy
+ .to raise_error(Users::DestroyService::DestroyError,
+ "Project #{project.id} can't be deleted" )
end
end
end
- end
- context 'projects in pending_delete' do
- before do
- project.pending_delete = true
- project.save!
- end
+ describe "calls the before/after callbacks" do
+ it 'of project_members' do
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
- it 'destroys a project in pending_delete' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
+ service.execute(user)
end
- service.execute(user)
+ it 'of group_members' do
+ group_member = create(:group_member)
+ group_member.group.group_members.create!(user: user, access_level: 40)
- expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
+
+ service.execute(user)
+ end
end
end
+ end
- context "a deleted user's issues" do
- let(:project) { create(:project) }
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
- before do
- project.add_developer(user)
- end
+ include_examples 'pre-migrate clean-up'
- context "for an issue the user was assigned to" do
- let!(:issue) { create(:issue, project: project, assignees: [user]) }
+ describe "Deletes a user and all their personal projects", :enable_admin_mode do
+ context 'no options are given' do
+ it 'deletes the user' do
+ user_data = service.execute(user)
- before do
- service.execute(user)
+ expect(user_data['email']).to eq(user.email)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'does not delete issues the user is assigned to' do
- expect(Issue.find_by_id(issue.id)).to be_present
+ it 'deletes user associations in batches' do
+ expect(user).to receive(:destroy_dependent_associations_in_batches)
+
+ service.execute(user)
end
- it 'migrates the issue so that it is "Unassigned"' do
- migrated_issue = Issue.find_by_id(issue.id)
+ it 'does not include snippets when deleting in batches' do
+ expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
- expect(migrated_issue.assignees).to be_empty
+ service.execute(user)
end
- end
- end
- context "a deleted user's merge_requests" do
- let(:project) { create(:project, :repository) }
+ it 'calls the bulk snippet destroy service for the user personal snippets' do
+ repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
+ repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
- before do
- project.add_developer(user)
- end
+ aggregate_failures do
+ expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_truthy
+ expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_truthy
+ end
- context "for an merge request the user was assigned to" do
- let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
+ # Call made when destroying user personal projects
+ expect(Snippets::BulkDestroyService).to receive(:new)
+ .with(admin, project.snippets).and_call_original
+
+ # Call to remove user personal snippets and for
+ # project snippets where projects are not user personal
+ # ones
+ expect(Snippets::BulkDestroyService).to receive(:new)
+ .with(admin, user.snippets.only_personal_snippets).and_call_original
- before do
service.execute(user)
+
+ aggregate_failures do
+ expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_falsey
+ expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_falsey
+ end
end
- it 'does not delete merge requests the user is assigned to' do
- expect(MergeRequest.find_by_id(merge_request.id)).to be_present
+ it 'calls the bulk snippet destroy service with hard delete option if it is present' do
+ # this avoids getting into Projects::DestroyService as it would
+ # call Snippets::BulkDestroyService first!
+ allow(user).to receive(:personal_projects).and_return([])
+
+ expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service|
+ expect(bulk_destroy_service).to receive(:execute).with({ skip_authorization: true }).and_call_original
+ end
+
+ service.execute(user, { hard_delete: true })
end
- it 'migrates the merge request so that it is "Unassigned"' do
- migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
+ it 'does not delete project snippets that the user is the author of' do
+ repo = create(:project_snippet, :repository, author: user).snippet_repository
+ service.execute(user)
+ expect(gitlab_shell.repository_exists?(repo.shard_name, repo.disk_path + '.git')).to be_truthy
+ expect(User.ghost.snippets).to include(repo.snippet)
+ end
- expect(migrated_merge_request.assignees).to be_empty
+ context 'when an error is raised deleting snippets' do
+ it 'does not delete user' do
+ snippet = create(:personal_snippet, :repository, author: user)
+
+ bulk_service = double
+ allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
+ allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
+ allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+
+ aggregate_failures do
+ expect { service.execute(user) }
+ .to raise_error(Users::DestroyService::DestroyError, 'foo' )
+ expect(snippet.reload).not_to be_nil
+ expect(
+ gitlab_shell.repository_exists?(snippet.repository_storage,
+ snippet.disk_path + '.git')
+ ).to be_truthy
+ end
+ end
end
end
- end
- context "solo owned groups present" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
+ context 'projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save!
+ end
- before do
- solo_owned.group_members = [member]
- end
+ it 'destroys a project in pending_delete' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
+ end
- it 'returns the user with attached errors' do
- expect(service.execute(user)).to be(user)
- expect(user.errors.full_messages).to eq([
- 'You must transfer ownership or delete groups before you can remove user'
- ])
- end
+ service.execute(user)
- it 'does not delete the user' do
- service.execute(user)
- expect(User.find(user.id)).to eq user
+ expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
- end
- context "deletions with solo owned groups" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
+ context "a deleted user's issues" do
+ let(:project) { create(:project) }
- before do
- solo_owned.group_members = [member]
- service.execute(user, delete_solo_owned_groups: true)
- end
+ before do
+ project.add_developer(user)
+ end
- it 'deletes solo owned groups' do
- expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ context "for an issue the user was assigned to" do
+ let!(:issue) { create(:issue, project: project, assignees: [user]) }
- it 'deletes the user' do
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
+ before do
+ service.execute(user)
+ end
- context 'deletions with inherited group owners' do
- let(:group) { create(:group, :nested) }
- let(:user) { create(:user) }
- let(:inherited_owner) { create(:user) }
+ it 'does not delete issues the user is assigned to' do
+ expect(Issue.find_by_id(issue.id)).to be_present
+ end
- before do
- group.parent.add_owner(inherited_owner)
- group.add_owner(user)
+ it 'migrates the issue so that it is "Unassigned"' do
+ migrated_issue = Issue.find_by_id(issue.id)
- service.execute(user, delete_solo_owned_groups: true)
+ expect(migrated_issue.assignees).to be_empty
+ end
+ end
end
- it 'does not delete the group' do
- expect(Group.exists?(id: group)).to be_truthy
- end
+ context "a deleted user's merge_requests" do
+ let(:project) { create(:project, :repository) }
- it 'deletes the user' do
- expect(User.exists?(id: user)).to be_falsey
- end
- end
+ before do
+ project.add_developer(user)
+ end
- context 'migrating associated records' do
- let!(:issue) { create(:issue, author: user) }
+ context "for an merge request the user was assigned to" do
+ let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
+
+ before do
+ service.execute(user)
+ end
- it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
+ it 'does not delete merge requests the user is assigned to' do
+ expect(MergeRequest.find_by_id(merge_request.id)).to be_present
+ end
- service.execute(user)
+ it 'migrates the merge request so that it is "Unassigned"' do
+ migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
- expect(issue.reload.author).to be_ghost
+ expect(migrated_merge_request.assignees).to be_empty
+ end
+ end
end
- context 'when hard_delete option is given' do
- it 'will not ghost certain records' do
+ context 'migrating associated records' do
+ let!(:issue) { create(:issue, author: user) }
+
+ it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
- service.execute(user, hard_delete: true)
+ service.execute(user)
- expect(Issue.exists?(issue.id)).to be_falsy
+ expect(issue.reload.author).to be_ghost
end
- end
- end
- describe "user personal's repository removal" do
- context 'storages' do
- before do
- perform_enqueued_jobs { service.execute(user) }
- end
+ context 'when hard_delete option is given' do
+ it 'will not ghost certain records' do
+ expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
- context 'legacy storage' do
- let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
+ service.execute(user, hard_delete: true)
- it 'removes repository' do
- expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(Issue.exists?(issue.id)).to be_falsy
end
end
+ end
+ end
- context 'hashed storage' do
- let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
+ describe "Deletion permission checks" do
+ it 'does not delete the user when user is not an admin' do
+ other_user = create(:user)
- it 'removes repository' do
- expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
- end
+ expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect(User.exists?(user.id)).to be(true)
+ end
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'allows admins to delete anyone' do
+ described_class.new(admin).execute(user)
+
+ expect(User.exists?(user.id)).to be(false)
end
end
- context 'repository removal status is taken into account' do
- it 'raises exception' do
- expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).and_return(false)
- end
+ context 'when admin mode is disabled' do
+ it 'disallows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- expect { service.execute(user) }
- .to raise_error(Users::DestroyService::DestroyError, "Project #{project.id} can't be deleted" )
+ expect(User.exists?(user.id)).to be(true)
end
end
- end
- describe "calls the before/after callbacks" do
- it 'of project_members' do
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
+ it 'allows users to delete their own account' do
+ described_class.new(user).execute(user)
- service.execute(user)
+ expect(User.exists?(user.id)).to be(false)
end
- it 'of group_members' do
- group_member = create(:group_member)
- group_member.group.group_members.create!(user: user, access_level: 40)
+ it 'allows user to be deleted if skip_authorization: true' do
+ other_user = create(:user)
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
+ described_class.new(user).execute(other_user, skip_authorization: true)
- service.execute(user)
+ expect(User.exists?(other_user.id)).to be(false)
end
end
- end
-
- describe "Deletion permission checks" do
- it 'does not delete the user when user is not an admin' do
- other_user = create(:user)
- expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(User.exists?(user.id)).to be(true)
- end
+ context 'batched nullify' do
+ let(:other_user) { create(:user) }
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'allows admins to delete anyone' do
- described_class.new(admin).execute(user)
+ it 'nullifies related associations in batches' do
+ expect(other_user).to receive(:nullify_dependent_associations_in_batches).and_call_original
- expect(User.exists?(user.id)).to be(false)
+ described_class.new(user).execute(other_user, skip_authorization: true)
end
- end
- context 'when admin mode is disabled' do
- it 'disallows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ it 'nullifies last_updated_issues, closed_issues, resource_label_events' do
+ issue = create(:issue, closed_by: other_user, updated_by: other_user)
+ resource_label_event = create(:resource_label_event, user: other_user)
- expect(User.exists?(user.id)).to be(true)
- end
- end
+ described_class.new(user).execute(other_user, skip_authorization: true)
- it 'allows users to delete their own account' do
- described_class.new(user).execute(user)
+ issue.reload
+ resource_label_event.reload
- expect(User.exists?(user.id)).to be(false)
+ expect(issue.closed_by).to be_nil
+ expect(issue.updated_by).to be_nil
+ expect(resource_label_event.user).to be_nil
+ end
end
+ end
- it 'allows user to be deleted if skip_authorization: true' do
- other_user = create(:user)
-
- described_class.new(user).execute(other_user, skip_authorization: true)
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ include_examples 'pre-migrate clean-up'
- expect(User.exists?(other_user.id)).to be(false)
+ describe "Deletes a user and all their personal projects", :enable_admin_mode do
+ context 'no options are given' do
+ it 'creates GhostUserMigration record to handle migration in a worker' do
+ expect { service.execute(user) }
+ .to(
+ change do
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ .exists?
+ end.from(false).to(true))
+ end
+ end
end
- end
- context 'batched nullify' do
- let(:other_user) { create(:user) }
+ describe "Deletion permission checks" do
+ it 'does not delete the user when user is not an admin' do
+ other_user = create(:user)
- it 'nullifies related associations in batches' do
- expect(other_user).to receive(:nullify_dependent_associations_in_batches).and_call_original
+ expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- described_class.new(user).execute(other_user, skip_authorization: true)
- end
+ expect(Users::GhostUserMigration).not_to be_exists
+ end
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'allows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }
+ .to(
+ change do
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ .exists?
+ end.from(false).to(true))
+ end
+ end
- it 'nullifies last_updated_issues, closed_issues, resource_label_events' do
- issue = create(:issue, closed_by: other_user, updated_by: other_user)
- resource_label_event = create(:resource_label_event, user: other_user)
+ context 'when admin mode is disabled' do
+ it 'disallows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- described_class.new(user).execute(other_user, skip_authorization: true)
+ expect(Users::GhostUserMigration).not_to be_exists
+ end
+ end
- issue.reload
- resource_label_event.reload
+ it 'allows users to delete their own account' do
+ expect { described_class.new(user).execute(user) }
+ .to(
+ change do
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: user)
+ .exists?
+ end.from(false).to(true))
+ end
- expect(issue.closed_by).to be_nil
- expect(issue.updated_by).to be_nil
- expect(resource_label_event.user).to be_nil
+ it 'allows user to be deleted if skip_authorization: true' do
+ other_user = create(:user)
+
+ expect do
+ described_class.new(user)
+ .execute(other_user, skip_authorization: true)
+ end.to(
+ change do
+ Users::GhostUserMigration.where(user: other_user,
+ initiator_user: user )
+ .exists?
+ end.from(false).to(true))
+ end
end
end
end
diff --git a/spec/services/users/email_verification/generate_token_service_spec.rb b/spec/services/users/email_verification/generate_token_service_spec.rb
new file mode 100644
index 00000000000..e7aa1bf8306
--- /dev/null
+++ b/spec/services/users/email_verification/generate_token_service_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::EmailVerification::GenerateTokenService do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:service) { described_class.new(attr: attr) }
+ let(:token) { 'token' }
+ let(:digest) { Devise.token_generator.digest(User, attr, token) }
+
+ describe '#execute' do
+ context 'with a valid attribute' do
+ where(:attr) { [:unlock_token, :confirmation_token] }
+
+ with_them do
+ before do
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:generate_token).and_return(token)
+ end
+ end
+
+ it "returns a token and it's digest" do
+ expect(service.execute).to eq([token, digest])
+ end
+ end
+ end
+
+ context 'with an invalid attribute' do
+ let(:attr) { :xxx }
+
+ it 'raises an error' do
+ expect { service.execute }.to raise_error(ArgumentError, 'Invalid attribute')
+ end
+ end
+ end
+end
diff --git a/spec/services/users/email_verification/validate_token_service_spec.rb b/spec/services/users/email_verification/validate_token_service_spec.rb
new file mode 100644
index 00000000000..44af4a4d36f
--- /dev/null
+++ b/spec/services/users/email_verification/validate_token_service_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::EmailVerification::ValidateTokenService, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:service) { described_class.new(attr: attr, user: user, token: token) }
+ let(:token) { 'token' }
+ let(:encrypted_token) { Devise.token_generator.digest(User, attr, token) }
+ let(:generated_at_attr) { attr == :unlock_token ? :locked_at : :confirmation_sent_at }
+ let(:token_generated_at) { 1.minute.ago }
+ let(:user) { build(:user, attr => encrypted_token, generated_at_attr => token_generated_at) }
+
+ describe '#execute' do
+ context 'with a valid attribute' do
+ where(:attr) { [:unlock_token, :confirmation_token] }
+
+ with_them do
+ context 'when successful' do
+ it 'returns a success status' do
+ expect(service.execute).to eq(status: :success)
+ end
+ end
+
+ context 'when rate limited' do
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?)
+ .with(:email_verification, scope: encrypted_token).and_return(true)
+ end
+
+ it 'returns a failure status' do
+ expect(service.execute).to eq(
+ {
+ status: :failure,
+ reason: :rate_limited,
+ message: format(s_("IdentityVerification|You've reached the maximum amount of tries. "\
+ 'Wait %{interval} or send a new code and try again.'), interval: '10 minutes')
+ }
+ )
+ end
+ end
+
+ context 'when expired' do
+ let(:token_generated_at) { 2.hours.ago }
+
+ it 'returns a failure status' do
+ expect(service.execute).to eq(
+ {
+ status: :failure,
+ reason: :expired,
+ message: s_('IdentityVerification|The code has expired. Send a new code and try again.')
+ }
+ )
+ end
+ end
+
+ context 'when invalid' do
+ let(:encrypted_token) { 'xxx' }
+
+ it 'returns a failure status' do
+ expect(service.execute).to eq(
+ {
+ status: :failure,
+ reason: :invalid,
+ message: s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')
+ }
+ )
+ end
+ end
+
+ context 'when encrypted token was not set and a blank token is provided' do
+ let(:encrypted_token) { nil }
+ let(:token) { '' }
+
+ it 'returns a failure status' do
+ expect(service.execute).to eq(
+ {
+ status: :failure,
+ reason: :invalid,
+ message: s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')
+ }
+ )
+ end
+ end
+ end
+ end
+
+ context 'with an invalid attribute' do
+ let(:attr) { :username }
+
+ it 'raises an error' do
+ expect { service.execute }.to raise_error(ArgumentError, 'Invalid attribute')
+ end
+ end
+ end
+end
diff --git a/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
new file mode 100644
index 00000000000..7366b1646b9
--- /dev/null
+++ b/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::MigrateRecordsToGhostUserInBatchesService do
+ let(:service) { described_class.new }
+
+ let_it_be(:ghost_user_migration) { create(:ghost_user_migration) }
+
+ describe '#execute' do
+ it 'stops when execution time limit reached' do
+ expect_next_instance_of(::Gitlab::Utils::ExecutionTracker) do |tracker|
+ expect(tracker).to receive(:over_limit?).and_return(true)
+ end
+
+ expect(Users::MigrateRecordsToGhostUserService).not_to receive(:new)
+
+ service.execute
+ end
+
+ it 'calls Users::MigrateRecordsToGhostUserService' do
+ expect_next_instance_of(Users::MigrateRecordsToGhostUserService) do |service|
+ expect(service).to(
+ receive(:execute)
+ .with(hard_delete: ghost_user_migration.hard_delete))
+ end
+
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
new file mode 100644
index 00000000000..766be51ae13
--- /dev/null
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::MigrateRecordsToGhostUserService do
+ let!(:user) { create(:user) }
+ let(:service) { described_class.new(user, admin, execution_tracker) }
+ let(:execution_tracker) { instance_double(::Gitlab::Utils::ExecutionTracker, over_limit?: false) }
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ context "when migrating a user's associated records to the ghost user" do
+ context 'for issues' do
+ context 'when deleted user is present as both author and edited_user' do
+ include_examples 'migrating records to the ghost user', Issue, [:author, :last_edited_by] do
+ let(:created_record) do
+ create(:issue, project: project, author: user, last_edited_by: user)
+ end
+ end
+ end
+
+ context 'when deleted user is present only as edited_user' do
+ include_examples 'migrating records to the ghost user', Issue, [:last_edited_by] do
+ let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) }
+ end
+ end
+
+ context "when deleted user is the assignee" do
+ let!(:issue) { create(:issue, project: project, assignees: [user]) }
+
+ it 'migrates the issue so that it is "Unassigned"' do
+ service.execute
+
+ migrated_issue = Issue.find_by_id(issue.id)
+ expect(migrated_issue).to be_present
+ expect(migrated_issue.assignees).to be_empty
+ end
+ end
+ end
+
+ context 'for merge requests' do
+ context 'when deleted user is present as both author and merge_user' do
+ include_examples 'migrating records to the ghost user', MergeRequest, [:author, :merge_user] do
+ let(:created_record) do
+ create(:merge_request, source_project: project,
+ author: user,
+ merge_user: user,
+ target_branch: "first")
+ end
+ end
+ end
+
+ context 'when deleted user is present only as both merge_user' do
+ include_examples 'migrating records to the ghost user', MergeRequest, [:merge_user] do
+ let(:created_record) do
+ create(:merge_request, source_project: project,
+ merge_user: user,
+ target_branch: "first")
+ end
+ end
+ end
+
+ context "when deleted user is the assignee" do
+ let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
+
+ it 'migrates the merge request so that it is "Unassigned"' do
+ service.execute
+
+ migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
+ expect(migrated_merge_request).to be_present
+ expect(migrated_merge_request.assignees).to be_empty
+ end
+ end
+ end
+
+ context 'for notes' do
+ include_examples 'migrating records to the ghost user', Note do
+ let(:created_record) { create(:note, project: project, author: user) }
+ end
+ end
+
+ context 'for abuse reports' do
+ include_examples 'migrating records to the ghost user', AbuseReport do
+ let(:created_record) { create(:abuse_report, reporter: user, user: create(:user)) }
+ end
+ end
+
+ context 'for award emoji' do
+ include_examples 'migrating records to the ghost user', AwardEmoji, [:user] do
+ let(:created_record) { create(:award_emoji, user: user) }
+
+ context "when the awardable already has an award emoji of the same name assigned to the ghost user" do
+ let(:awardable) { create(:issue) }
+ let!(:existing_award_emoji) { create(:award_emoji, user: User.ghost, name: "thumbsup", awardable: awardable) }
+ let!(:award_emoji) { create(:award_emoji, user: user, name: "thumbsup", awardable: awardable) }
+
+ it "migrates the award emoji regardless" do
+ service.execute
+
+ migrated_record = AwardEmoji.find_by_id(award_emoji.id)
+
+ expect(migrated_record.user).to eq(User.ghost)
+ end
+
+ it "does not leave the migrated award emoji in an invalid state" do
+ service.execute
+
+ migrated_record = AwardEmoji.find_by_id(award_emoji.id)
+
+ expect(migrated_record).to be_valid
+ end
+ end
+ end
+ end
+
+ context 'for snippets' do
+ include_examples 'migrating records to the ghost user', Snippet do
+ let(:created_record) { create(:snippet, project: project, author: user) }
+ end
+ end
+
+ context 'for reviews' do
+ include_examples 'migrating records to the ghost user', Review, [:author] do
+ let(:created_record) { create(:review, author: user) }
+ end
+ end
+ end
+
+ context 'on post-migrate cleanups' do
+ it 'destroys the user and personal namespace' do
+ namespace = user.namespace
+
+ allow(user).to receive(:destroy).and_call_original
+
+ service.execute
+
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'deletes user associations in batches' do
+ expect(user).to receive(:destroy_dependent_associations_in_batches)
+
+ service.execute
+ end
+
+ context 'for batched nullify' do
+ it 'nullifies related associations in batches' do
+ expect(user).to receive(:nullify_dependent_associations_in_batches).and_call_original
+
+ service.execute
+ end
+
+ it 'nullifies last_updated_issues, closed_issues, resource_label_events' do
+ issue = create(:issue, closed_by: user, updated_by: user)
+ resource_label_event = create(:resource_label_event, user: user)
+
+ service.execute
+
+ issue.reload
+ resource_label_event.reload
+
+ expect(issue.closed_by).to be_nil
+ expect(issue.updated_by).to be_nil
+ expect(resource_label_event.user).to be_nil
+ end
+ end
+
+ context 'for snippets' do
+ let(:gitlab_shell) { Gitlab::Shell.new }
+
+ it 'does not include snippets when deleting in batches' do
+ expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
+
+ service.execute
+ end
+
+ it 'calls the bulk snippet destroy service for the user personal snippets' do
+ repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
+ repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
+
+ aggregate_failures do
+ expect(gitlab_shell.repository_exists?(repo1.shard_name, "#{repo1.disk_path}.git")).to be(true)
+ expect(gitlab_shell.repository_exists?(repo2.shard_name, "#{repo2.disk_path}.git")).to be(true)
+ end
+
+ # Call made when destroying user personal projects
+ expect(Snippets::BulkDestroyService).not_to(
+ receive(:new).with(admin, project.snippets).and_call_original)
+
+ # Call to remove user personal snippets and for
+ # project snippets where projects are not user personal
+ # ones
+ expect(Snippets::BulkDestroyService).to(
+ receive(:new).with(admin, user.snippets.only_personal_snippets).and_call_original)
+
+ service.execute
+
+ aggregate_failures do
+ expect(gitlab_shell.repository_exists?(repo1.shard_name, "#{repo1.disk_path}.git")).to be(false)
+ expect(gitlab_shell.repository_exists?(repo2.shard_name, "#{repo2.disk_path}.git")).to be(true)
+ end
+ end
+
+ it 'calls the bulk snippet destroy service with hard delete option if it is present' do
+ # this avoids getting into Projects::DestroyService as it would
+ # call Snippets::BulkDestroyService first!
+ allow(user).to receive(:personal_projects).and_return([])
+
+ expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service|
+ expect(bulk_destroy_service).to receive(:execute).with({ skip_authorization: true }).and_call_original
+ end
+
+ service.execute(hard_delete: true)
+ end
+
+ it 'does not delete project snippets that the user is the author of' do
+ repo = create(:project_snippet, :repository, author: user).snippet_repository
+
+ service.execute
+
+ expect(gitlab_shell.repository_exists?(repo.shard_name, "#{repo.disk_path}.git")).to be(true)
+ expect(User.ghost.snippets).to include(repo.snippet)
+ end
+
+ context 'when an error is raised deleting snippets' do
+ it 'does not delete user' do
+ snippet = create(:personal_snippet, :repository, author: user)
+
+ bulk_service = double
+ allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
+ allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
+ allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+
+ aggregate_failures do
+ expect { service.execute }.to(
+ raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo' ))
+ expect(snippet.reload).not_to be_nil
+ expect(
+ gitlab_shell.repository_exists?(snippet.repository_storage,
+ "#{snippet.disk_path}.git")
+ ).to be(true)
+ end
+ end
+ end
+ end
+
+ context 'when hard_delete option is given' do
+ it 'will not ghost certain records' do
+ issue = create(:issue, author: user)
+
+ service.execute(hard_delete: true)
+
+ expect(Issue).not_to exist(issue.id)
+ end
+ end
+ end
+end
diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb
index 5a243e876ac..abff6b1e023 100644
--- a/spec/services/users/reject_service_spec.rb
+++ b/spec/services/users/reject_service_spec.rb
@@ -35,11 +35,29 @@ RSpec.describe Users::RejectService do
context 'success' do
context 'when the executor user is an admin in admin mode', :enable_admin_mode do
- it 'deletes the user', :sidekiq_inline do
- subject
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates user removal', :sidekiq_inline do
+ subject
+
+ expect(subject[:status]).to eq(:success)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: current_user)
+ ).to be_exists
+ end
+ end
- expect(subject[:status]).to eq(:success)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'deletes the user', :sidekiq_inline do
+ subject
+
+ expect(subject[:status]).to eq(:success)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
it 'emails the user on rejection' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 2e0b0051495..e8b82b0b4f2 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -54,6 +54,12 @@ RSpec.describe WorkItems::UpdateService do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_title_changed_action)
expect(update_work_item[:status]).to eq(:success)
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_TITLE_CHANGED }
+ let(:user) { current_user }
+ subject(:service_action) { update_work_item[:status] }
+ end
end
context 'when title is not changed' do
@@ -64,6 +70,12 @@ RSpec.describe WorkItems::UpdateService do
expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_title_changed_action)
expect(update_work_item[:status]).to eq(:success)
end
+
+ it 'does not emit Snowplow event', :snowplow do
+ expect_no_snowplow_event
+
+ update_work_item
+ end
end
context 'when dates are changed' do
diff --git a/spec/services/work_items/widgets/description_service/update_service_spec.rb b/spec/services/work_items/widgets/description_service/update_service_spec.rb
index 582d9dc85f7..4275950e720 100644
--- a/spec/services/work_items/widgets/description_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/description_service/update_service_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService do
let(:current_user) { author }
let(:work_item) do
create(:work_item, author: author, project: project, description: 'old description',
- last_edited_at: Date.yesterday, last_edited_by: random_user
+ last_edited_at: Date.yesterday, last_edited_by: random_user
)
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8acf3bcf9c0..c75f651fb92 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -36,6 +36,7 @@ require 'rspec-parameterized'
require 'shoulda/matchers'
require 'test_prof/recipes/rspec/let_it_be'
require 'test_prof/factory_default'
+require 'test_prof/factory_prof/nate_heckler'
require 'parslet/rig/rspec'
rspec_profiling_is_configured =
@@ -53,6 +54,9 @@ end
require 'rainbow/ext/string'
Rainbow.enabled = false
+# Enable zero monkey patching mode before loading any other RSpec code.
+RSpec.configure(&:disable_monkey_patching!)
+
require_relative('../ee/spec/spec_helper') if Gitlab.ee?
require_relative('../jh/spec/spec_helper') if Gitlab.jh?
@@ -89,30 +93,6 @@ RSpec.configure do |config|
config.full_backtrace = true
end
- # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/297359
- if ENV['CI']
- config.after do |example|
- if example.exception.is_a?(GRPC::Unavailable)
- warn "=== gRPC unavailable detected, process list:"
- processes = `ps -ef | grep toml`
- warn processes
- warn "=== free memory"
- warn `free -m`
- warn "=== uptime"
- warn `uptime`
- warn "=== Prometheus metrics:"
- warn `curl -s -o log/gitaly-metrics.log http://localhost:9236/metrics`
- warn "=== Taking goroutine dump in log/goroutines.log..."
- warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
- end
- end
- else
- # Allow running `:focus` examples locally,
- # falling back to all tests when there is no `:focus` example.
- config.filter_run focus: true
- config.run_all_when_everything_filtered = true
- end
-
# Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531
config.after do |example|
if example.exception.is_a?(Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError)
@@ -122,9 +102,6 @@ RSpec.configure do |config|
end
end
- # Re-run failures locally with `--only-failures`
- config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt')
-
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata|
location = metadata[:location]
@@ -170,6 +147,7 @@ RSpec.configure do |config|
config.include TestEnv
config.include FileReadHelpers
config.include Database::MultipleDatabases
+ config.include Database::WithoutCheckConstraint
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :feature
@@ -397,6 +375,12 @@ RSpec.configure do |config|
example.run unless GitalySetup.praefect_with_db?
end
+ config.around(:example, :yaml_processor_feature_flag_corectness) do |example|
+ ::Gitlab::Ci::YamlProcessor::FeatureFlags.ensure_correct_usage do
+ example.run
+ end
+ end
+
# previous test runs may have left some resources throttled
config.before do
::Gitlab::ExclusiveLease.reset_all!("el:throttle:*")
@@ -478,8 +462,6 @@ RSpec.configure do |config|
config.before(:each, :js) do
allow_any_instance_of(VersionCheck).to receive(:response).and_return({ "severity" => "success" })
end
-
- config.disable_monkey_patching!
end
ActiveRecord::Migration.maintain_test_schema!
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 14ef0f1b7e0..a5d845f5177 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -72,6 +72,12 @@ Capybara.register_driver :chrome do |app|
# Explicitly set user-data-dir to prevent crashes. See https://gitlab.com/gitlab-org/gitlab-foss/issues/58882#note_179811508
options.add_argument("user-data-dir=/tmp/chrome") if ENV['CI'] || ENV['CI_SERVER']
+ # Set chrome default download path
+ if ENV['DEFAULT_CHROME_DOWNLOAD_PATH']
+ options.add_preference("download.default_directory", ENV['DEFAULT_CHROME_DOWNLOAD_PATH'])
+ options.add_preference("download.prompt_for_download", false)
+ end
+
# Chrome 75 defaults to W3C mode which doesn't allow console log access
options.add_option(:w3c, false)
diff --git a/spec/support/database/without_check_constraint.rb b/spec/support/database/without_check_constraint.rb
new file mode 100644
index 00000000000..b361f4374b8
--- /dev/null
+++ b/spec/support/database/without_check_constraint.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+# Temporarily disable the named constraint on the table within the block.
+#
+# without_constraint('members', 'check_1234') do
+# create_invalid_data
+# end
+module Database
+ module WithoutCheckConstraint
+ def without_check_constraint(table, name, connection:)
+ saved_constraint = constraint(table, name, connection)
+
+ constraint_error!(table, name, connection) if saved_constraint.nil?
+
+ begin
+ connection.remove_check_constraint(table, name: name)
+ connection.transaction do
+ yield
+ raise ActiveRecord::Rollback
+ end
+ ensure
+ restore_constraint(saved_constraint, connection)
+ end
+ end
+
+ private
+
+ def constraint_error!(table, name, connection)
+ msg = if connection.table_exists?(table)
+ "'#{table}' table does not contain constraint called '#{name}'"
+ else
+ "'#{table}' does not exist"
+ end
+
+ raise msg
+ end
+
+ def constraint(table, name, connection)
+ connection
+ .check_constraints(table)
+ .find { |constraint| constraint.options[:name] == name }
+ end
+
+ def restore_constraint(constraint, connection)
+ connection.add_check_constraint(
+ constraint.table_name,
+ constraint.expression,
+ **constraint.options
+ )
+ end
+ end
+end
diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml
index b1533879e32..b6a66cfa2c6 100644
--- a/spec/support/gitlab_stubs/gitlab_ci.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci.yml
@@ -8,6 +8,9 @@ before_script:
variables:
DB_NAME: postgres
+ ENVIRONMENT_VAR:
+ value: 'env var value'
+ description: 'env var description'
stages:
- test
diff --git a/spec/support/helpers/api_internal_base_helpers.rb b/spec/support/helpers/api_internal_base_helpers.rb
index 94996f7480e..e89716571f9 100644
--- a/spec/support/helpers/api_internal_base_helpers.rb
+++ b/spec/support/helpers/api_internal_base_helpers.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
+require_relative 'gitlab_shell_helpers'
+
module APIInternalBaseHelpers
+ include GitlabShellHelpers
+
def gl_repository_for(container)
case container
when ProjectWiki
@@ -33,9 +37,9 @@ module APIInternalBaseHelpers
project: full_path_for(container),
gl_repository: gl_repository_for(container),
action: 'git-upload-pack',
- secret_token: secret_token,
protocol: protocol
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
)
end
@@ -56,7 +60,6 @@ module APIInternalBaseHelpers
key_id: key.id,
project: full_path,
action: 'git-receive-pack',
- secret_token: secret_token,
protocol: protocol,
env: env
}
@@ -64,7 +67,8 @@ module APIInternalBaseHelpers
post(
api("/internal/allowed"),
- params: params
+ params: params,
+ headers: gitlab_shell_internal_api_request_header
)
end
@@ -77,9 +81,9 @@ module APIInternalBaseHelpers
project: full_path_for(container),
gl_repository: gl_repository_for(container),
action: 'git-upload-archive',
- secret_token: secret_token,
protocol: 'ssh'
- }
+ },
+ headers: gitlab_shell_internal_api_request_header
)
end
end
diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb
index 119f8d001a1..2e9b6f748cd 100644
--- a/spec/support/helpers/ci/template_helpers.rb
+++ b/spec/support/helpers/ci/template_helpers.rb
@@ -2,10 +2,6 @@
module Ci
module TemplateHelpers
- def secure_analyzers_prefix
- 'registry.gitlab.com/security-products'
- end
-
def template_registry_host
'registry.gitlab.com'
end
diff --git a/spec/support/helpers/create_environments_helpers.rb b/spec/support/helpers/create_environments_helpers.rb
index be105f5862b..361d365dc5b 100644
--- a/spec/support/helpers/create_environments_helpers.rb
+++ b/spec/support/helpers/create_environments_helpers.rb
@@ -7,7 +7,7 @@ module CreateEnvironmentsHelpers
start_review = create(:ci_build, :start_review_app, :success, **common, pipeline: pipeline)
stop_review = create(:ci_build, :stop_review_app, :manual, **common, pipeline: pipeline)
environment = create(:environment, :auto_stoppable, project: project, name: ref)
- create(:deployment, :success, **common, on_stop: stop_review.name,
- deployable: start_review, environment: environment)
+ create(:deployment, :success, **common,
+ on_stop: stop_review.name, deployable: start_review, environment: environment)
end
end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 05e9a099a2b..6d41d7b7414 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -7,7 +7,7 @@ module CycleAnalyticsHelpers
def path_nav_stage_names_without_median
# Returns the path names with the median value stripped out
- page.all('.gl-path-button').collect(&:text).map {|name_with_median| name_with_median.split("\n")[0] }
+ page.all('.gl-path-button').collect(&:text).map { |name_with_median| name_with_median.split("\n")[0] }
end
def fill_in_custom_stage_fields
diff --git a/spec/support/helpers/database/partitioning_helpers.rb b/spec/support/helpers/database/partitioning_helpers.rb
index 80b31fe0603..889652a9252 100644
--- a/spec/support/helpers/database/partitioning_helpers.rb
+++ b/spec/support/helpers/database/partitioning_helpers.rb
@@ -79,8 +79,8 @@ module Database
SQL
end
- def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)
- connection.select_rows(<<~SQL)
+ def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA, conn: connection)
+ conn.select_rows(<<~SQL)
select
pg_class.relname
from pg_class
diff --git a/spec/support/helpers/gitlab_shell_helpers.rb b/spec/support/helpers/gitlab_shell_helpers.rb
new file mode 100644
index 00000000000..aa0cec22727
--- /dev/null
+++ b/spec/support/helpers/gitlab_shell_helpers.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module GitlabShellHelpers
+ extend self
+
+ def gitlab_shell_internal_api_request_header(
+ issuer: API::Helpers::GITLAB_SHELL_JWT_ISSUER, secret_token: Gitlab::Shell.secret_token)
+ jwt_token = JSONWebToken::HMACToken.new(secret_token).tap do |token|
+ token.issuer = issuer
+ end
+
+ { API::Helpers::GITLAB_SHELL_API_HEADER => jwt_token.encoded }
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index d78c523decd..9d745f2cb70 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -130,11 +130,12 @@ module GraphqlHelpers
current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
schema: GitlabSchema, # A specific schema instance
object_type: described_class, # The `BaseObject` type this field belongs to
- arg_style: :internal_prepared # Args are in internal format, but should use more rigorous processing
+ arg_style: :internal_prepared, # Args are in internal format, but should use more rigorous processing
+ query: nil # Query to evaluate the field
)
field = to_base_field(field, object_type)
ctx[:current_user] = current_user unless current_user == :not_given
- query = GraphQL::Query.new(schema, context: ctx.to_h)
+ query ||= GraphQL::Query.new(schema, context: ctx.to_h)
extras[:lookahead] = negative_lookahead if extras[:lookahead] == :not_given && field.extras.include?(:lookahead)
query_ctx = query.context
@@ -857,6 +858,8 @@ module GraphqlHelpers
def positive_lookahead
double(selects?: true).tap do |selection|
allow(selection).to receive(:selection).and_return(selection)
+ allow(selection).to receive(:selections).and_return(selection)
+ allow(selection).to receive(:map).and_return(double(include?: true))
end
end
diff --git a/spec/support/helpers/html_escaped_helpers.rb b/spec/support/helpers/html_escaped_helpers.rb
new file mode 100644
index 00000000000..7f6825e9598
--- /dev/null
+++ b/spec/support/helpers/html_escaped_helpers.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module HtmlEscapedHelpers
+ extend self
+
+ # Checks if +content+ contains HTML escaped tags and returns its match.
+ #
+ # It matches escaped opening and closing tags `&lt;<name>` and
+ # `&lt;/<name>`. The match is discarded if the tag is inside a quoted
+ # attribute value.
+ # Foor example, `<div title="We allow # &lt;b&gt;bold&lt;/b&gt;">`.
+ #
+ # @return [MatchData, nil] Returns the match or +nil+ if no match was found.
+ def match_html_escaped_tags(content)
+ match_data = %r{&lt;\s*(?:/\s*)?\w+}.match(content)
+ return unless match_data
+
+ # Escaped HTML tags are allowed inside quoted attribute values like:
+ # `title="Press &lt;back&gt;"`
+ return if %r{=\s*["'][^>]*\z}.match?(match_data.pre_match)
+
+ match_data
+ end
+end
diff --git a/spec/support/helpers/javascript_form_helper.rb b/spec/support/helpers/javascript_form_helper.rb
new file mode 100644
index 00000000000..41c5ba4373b
--- /dev/null
+++ b/spec/support/helpers/javascript_form_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module JavascriptFormHelper
+ def prevent_submit_for(query_selector)
+ execute_script("document.querySelector('#{query_selector}').addEventListener('submit', e => e.preventDefault())")
+ end
+end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index dd210f02ae7..912e7d24b25 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -880,7 +880,7 @@ module KubernetesHelpers
containers.map do |container|
terminal = {
selectors: { pod: pod_name, container: container['name'] },
- url: container_exec_url(service.api_url, pod_namespace, pod_name, container['name']),
+ url: container_exec_url(service.api_url, pod_namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index d966fd13dca..87a1f5459ec 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -122,7 +122,7 @@ module LoginHelpers
def register_via(provider, uid, email, additional_info: {})
mock_auth_hash(provider, uid, email, additional_info: additional_info)
visit new_user_registration_path
- expect(page).to have_content('Create an account using')
+ expect(page).to have_content('Create an account using').or(have_content('Register with'))
click_link_or_button "oauth-login-#{provider}"
end
diff --git a/spec/support/helpers/migrations_helpers/work_item_types_helper.rb b/spec/support/helpers/migrations_helpers/work_item_types_helper.rb
index 59b1f1b1305..b05caf265ee 100644
--- a/spec/support/helpers/migrations_helpers/work_item_types_helper.rb
+++ b/spec/support/helpers/migrations_helpers/work_item_types_helper.rb
@@ -3,11 +3,11 @@
module MigrationHelpers
module WorkItemTypesHelper
DEFAULT_WORK_ITEM_TYPES = {
- issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
- incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
- test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 },
+ issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
+ incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
+ test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 },
requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 },
- task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }
+ task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }
}.freeze
def reset_work_item_types
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index e11548d0b75..b44552d6479 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -34,7 +34,7 @@ module NavbarStructureHelper
insert_after_nav_item(
within,
new_nav_item: {
- nav_item: _('Packages & Registries'),
+ nav_item: _('Packages and registries'),
nav_sub_items: [_('Package Registry')]
}
)
@@ -56,7 +56,7 @@ module NavbarStructureHelper
def insert_container_nav
insert_after_sub_nav_item(
_('Package Registry'),
- within: _('Packages & Registries'),
+ within: _('Packages and registries'),
new_sub_nav_item_name: _('Container Registry')
)
end
@@ -64,7 +64,7 @@ module NavbarStructureHelper
def insert_dependency_proxy_nav
insert_after_sub_nav_item(
_('Package Registry'),
- within: _('Packages & Registries'),
+ within: _('Packages and registries'),
new_sub_nav_item_name: _('Dependency Proxy')
)
end
@@ -72,7 +72,7 @@ module NavbarStructureHelper
def insert_infrastructure_registry_nav
insert_after_sub_nav_item(
_('Package Registry'),
- within: _('Packages & Registries'),
+ within: _('Packages and registries'),
new_sub_nav_item_name: _('Infrastructure Registry')
)
end
@@ -80,11 +80,21 @@ module NavbarStructureHelper
def insert_harbor_registry_nav(within)
insert_after_sub_nav_item(
within,
- within: _('Packages & Registries'),
+ within: _('Packages and registries'),
new_sub_nav_item_name: _('Harbor Registry')
)
end
+ def insert_observability_nav
+ insert_after_nav_item(
+ _('Kubernetes'),
+ new_nav_item: {
+ nav_item: _('Observability'),
+ nav_sub_items: []
+ }
+ )
+ end
+
def insert_infrastructure_google_cloud_nav
insert_after_sub_nav_item(
_('Terraform'),
diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb
index 59723583cbc..9628762d46a 100644
--- a/spec/support/helpers/seed_helper.rb
+++ b/spec/support/helpers/seed_helper.rb
@@ -29,8 +29,8 @@ module SeedHelper
def create_bare_seeds
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
+ out: '/dev/null',
+ err: '/dev/null')
end
def create_normal_seeds
diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb
index c8b194919ed..265e1c38b09 100644
--- a/spec/support/helpers/snowplow_helpers.rb
+++ b/spec/support/helpers/snowplow_helpers.rb
@@ -7,7 +7,7 @@ module SnowplowHelpers
#
# Examples:
#
- # describe '#show', :snowplow do
+ # describe '#show' do
# it 'tracks snowplow events' do
# get :show
#
@@ -15,7 +15,7 @@ module SnowplowHelpers
# end
# end
#
- # describe '#create', :snowplow do
+ # describe '#create' do
# it 'tracks snowplow events' do
# post :create
#
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index 20f46396424..c08e35912c3 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -104,6 +104,10 @@ module StubConfiguration
.to receive(:sentry_clientside_dsn) { clientside_dsn }
end
+ def stub_microsoft_graph_mailer_setting(messages)
+ allow(Gitlab.config.microsoft_graph_mailer).to receive_messages(to_settings(messages))
+ end
+
def stub_kerberos_setting(messages)
allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages))
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 024f06cae1b..661c1c683b0 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -3,8 +3,8 @@
module StubObjectStorage
def stub_dependency_proxy_object_storage(**params)
stub_object_storage_uploader(config: ::Gitlab.config.dependency_proxy.object_store,
- uploader: ::DependencyProxy::FileUploader,
- **params)
+ uploader: ::DependencyProxy::FileUploader,
+ **params)
end
def stub_object_storage_uploader(
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 03e9ad1a08e..691f978550a 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -10,85 +10,86 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'signed-commits' => 'c7794c1',
- 'not-merged-branch' => 'b83d6e3',
- 'branch-merged' => '498214d',
- 'empty-branch' => '7efb185',
- 'ends-with.json' => '98b0d8b',
- 'flatten-dir' => 'e56497b',
- 'feature' => '0b4bc9a',
- 'feature_conflict' => 'bb5206f',
- 'fix' => '48f0be4',
- 'improve/awesome' => '5937ac0',
- 'merged-target' => '21751bf',
- 'markdown' => '0ed8c6c',
- 'lfs' => '55bc176',
- 'master' => 'b83d6e3',
- 'merge-test' => '5937ac0',
- "'test'" => 'e56497b',
- 'orphaned-branch' => '45127a9',
- 'binary-encoding' => '7b1cf43',
- 'gitattributes' => '5a62481',
- 'expand-collapse-diffs' => '4842455',
- 'symlink-expand-diff' => '81e6355',
- 'diff-files-symlink-to-image' => '8cfca84',
- 'diff-files-image-to-symlink' => '3e94fda',
- 'diff-files-symlink-to-text' => '689815e',
- 'diff-files-text-to-symlink' => '5e2c270',
- 'expand-collapse-files' => '025db92',
- 'expand-collapse-lines' => '238e82d',
- 'pages-deploy' => '7897d5b',
- 'pages-deploy-target' => '7975be0',
- 'audio' => 'c3c21fd',
- 'video' => '8879059',
- 'crlf-diff' => '5938907',
- 'conflict-start' => '824be60',
- 'conflict-resolvable' => '1450cd6',
- 'conflict-binary-file' => '259a6fb',
+ 'signed-commits' => 'c7794c1',
+ 'not-merged-branch' => 'b83d6e3',
+ 'branch-merged' => '498214d',
+ 'empty-branch' => '7efb185',
+ 'ends-with.json' => '98b0d8b',
+ 'flatten-dir' => 'e56497b',
+ 'feature' => '0b4bc9a',
+ 'feature_conflict' => 'bb5206f',
+ 'fix' => '48f0be4',
+ 'improve/awesome' => '5937ac0',
+ 'merged-target' => '21751bf',
+ 'markdown' => '0ed8c6c',
+ 'lfs' => '55bc176',
+ 'master' => 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+ 'merge-test' => '5937ac0',
+ "'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
+ 'expand-collapse-diffs' => '4842455',
+ 'symlink-expand-diff' => '81e6355',
+ 'diff-files-symlink-to-image' => '8cfca84',
+ 'diff-files-image-to-symlink' => '3e94fda',
+ 'diff-files-symlink-to-text' => '689815e',
+ 'diff-files-text-to-symlink' => '5e2c270',
+ 'expand-collapse-files' => '025db92',
+ 'expand-collapse-lines' => '238e82d',
+ 'pages-deploy' => '7897d5b',
+ 'pages-deploy-target' => '7975be0',
+ 'audio' => 'c3c21fd',
+ 'video' => '8879059',
+ 'crlf-diff' => '5938907',
+ 'conflict-start' => '824be60',
+ 'conflict-resolvable' => '1450cd6',
+ 'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '78a3086',
- 'conflict-missing-side' => 'eb227b3',
- 'conflict-non-utf8' => 'd0a293c',
- 'conflict-too-large' => '39fa04f',
- 'deleted-image-test' => '6c17798',
- 'wip' => 'b9238ee',
- 'csv' => '3dd0896',
- 'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => '4963fef',
- 'add-pdf-file' => 'e774ebd',
- 'squash-large-files' => '54cec52',
- 'add-pdf-text-binary' => '79faa7b',
- 'add_images_and_changes' => '010d106',
- 'update-gitlab-shell-v-6-0-1' => '2f61d70',
- 'update-gitlab-shell-v-6-0-3' => 'de78448',
- 'merge-commit-analyze-before' => '1adbdef',
- 'merge-commit-analyze-side-branch' => '8a99451',
- 'merge-commit-analyze-after' => '646ece5',
- 'snippet/single-file' => '43e4080aaa14fc7d4b77ee1f5c9d067d5a7df10e',
- 'snippet/multiple-files' => '40232f7eb98b3f221886432def6e8bab2432add9',
- 'snippet/rename-and-edit-file' => '220a1e4b4dff37feea0625a7947a4c60fbe78365',
- 'snippet/edit-file' => 'c2f074f4f26929c92795a75775af79a6ed6d8430',
- 'snippet/no-files' => '671aaa842a4875e5f30082d1ab6feda345fdb94d',
- '2-mb-file' => 'bf12d25',
- 'before-create-delete-modify-move' => '845009f',
- 'between-create-delete-modify-move' => '3f5f443',
- 'after-create-delete-modify-move' => 'ba3faa7',
- 'with-codeowners' => '219560e',
- 'submodule_inside_folder' => 'b491b92',
- 'png-lfs' => 'fe42f41',
- 'sha-starting-with-large-number' => '8426165',
- 'invalid-utf8-diff-paths' => '99e4853',
- 'compare-with-merge-head-source' => 'f20a03d',
- 'compare-with-merge-head-target' => '2f1e176',
- 'trailers' => 'f0a5ed6',
- 'add_commit_with_5mb_subject' => '8cf8e80',
- 'blame-on-renamed' => '32c33da',
- 'with-executables' => '6b8dc4a',
- 'spooky-stuff' => 'ba3343b',
- 'few-commits' => '0031876',
- 'two-commits' => '304d257',
- 'utf-16' => 'f05a987',
- 'gitaly-rename-test' => '94bb47c',
- 'smime-signed-commits' => 'ed775cc'
+ 'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
+ 'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798',
+ 'wip' => 'b9238ee',
+ 'csv' => '3dd0896',
+ 'v1.1.0' => 'b83d6e3',
+ 'add-ipython-files' => '4963fef',
+ 'add-pdf-file' => 'e774ebd',
+ 'squash-large-files' => '54cec52',
+ 'add-pdf-text-binary' => '79faa7b',
+ 'add_images_and_changes' => '010d106',
+ 'update-gitlab-shell-v-6-0-1' => '2f61d70',
+ 'update-gitlab-shell-v-6-0-3' => 'de78448',
+ 'merge-commit-analyze-before' => '1adbdef',
+ 'merge-commit-analyze-side-branch' => '8a99451',
+ 'merge-commit-analyze-after' => '646ece5',
+ 'snippet/single-file' => '43e4080aaa14fc7d4b77ee1f5c9d067d5a7df10e',
+ 'snippet/multiple-files' => '40232f7eb98b3f221886432def6e8bab2432add9',
+ 'snippet/rename-and-edit-file' => '220a1e4b4dff37feea0625a7947a4c60fbe78365',
+ 'snippet/edit-file' => 'c2f074f4f26929c92795a75775af79a6ed6d8430',
+ 'snippet/no-files' => '671aaa842a4875e5f30082d1ab6feda345fdb94d',
+ '2-mb-file' => 'bf12d25',
+ 'before-create-delete-modify-move' => '845009f',
+ 'between-create-delete-modify-move' => '3f5f443',
+ 'after-create-delete-modify-move' => 'ba3faa7',
+ 'with-codeowners' => '219560e',
+ 'submodule_inside_folder' => 'b491b92',
+ 'png-lfs' => 'fe42f41',
+ 'sha-starting-with-large-number' => '8426165',
+ 'invalid-utf8-diff-paths' => '99e4853',
+ 'compare-with-merge-head-source' => 'f20a03d',
+ 'compare-with-merge-head-target' => '2f1e176',
+ 'trailers' => 'f0a5ed6',
+ 'add_commit_with_5mb_subject' => '8cf8e80',
+ 'blame-on-renamed' => '32c33da',
+ 'with-executables' => '6b8dc4a',
+ 'spooky-stuff' => 'ba3343b',
+ 'few-commits' => '0031876',
+ 'two-commits' => '304d257',
+ 'utf-16' => 'f05a987',
+ 'gitaly-rename-test' => '94bb47c',
+ 'smime-signed-commits' => 'ed775cc',
+ 'Ääh-test-utf-8' => '7975be0'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -96,9 +97,9 @@ module TestEnv
# We currently only need a subset of the branches
FORKED_BRANCH_SHA = {
'add-submodule-version-bump' => '3f547c0',
- 'master' => '5937ac0',
- 'remove-submodule' => '2a33e0c',
- 'conflict-resolvable-fork' => '404fa3f'
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c',
+ 'conflict-resolvable-fork' => '404fa3f'
}.freeze
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 2a9144614d0..1aea3545ae0 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -6,22 +6,14 @@ module UsageDataHelpers
snippet_update
snippet_comment
merge_request_comment
- merge_request_create
commit_comment
wiki_pages_create
wiki_pages_update
wiki_pages_delete
- web_ide_views
- web_ide_commits
- web_ide_merge_requests
- web_ide_previews
navbar_searches
cycle_analytics_views
productivity_analytics_views
source_code_pushes
- design_management_designs_create
- design_management_designs_update
- design_management_designs_delete
).freeze
COUNTS_KEYS = %i(
@@ -126,7 +118,6 @@ module UsageDataHelpers
uploads
web_hooks
user_preferences_user_gitpod_enabled
- service_usage_data_download_payload_click
).push(*SMAU_KEYS)
USAGE_DATA_KEYS = %i(
@@ -193,11 +184,11 @@ module UsageDataHelpers
allow(Settings).to receive(:[]).with('artifacts')
.and_return(
{ 'enabled' => true,
- 'object_store' =>
+ 'object_store' =>
{ 'enabled' => true,
- 'remote_directory' => 'artifacts',
- 'direct_upload' => true,
- 'connection' =>
+ 'remote_directory' => 'artifacts',
+ 'direct_upload' => true,
+ 'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
'background_upload' => false,
'proxy_download' => false } }
@@ -208,11 +199,11 @@ module UsageDataHelpers
allow(Settings).to receive(:[]).with('lfs')
.and_return(
{ 'enabled' => true,
- 'object_store' =>
+ 'object_store' =>
{ 'enabled' => false,
- 'remote_directory' => 'lfs-objects',
- 'direct_upload' => true,
- 'connection' =>
+ 'remote_directory' => 'lfs-objects',
+ 'direct_upload' => true,
+ 'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
'background_upload' => false,
'proxy_download' => false } }
@@ -221,21 +212,21 @@ module UsageDataHelpers
.and_return(
{ 'object_store' =>
{ 'enabled' => false,
- 'remote_directory' => 'uploads',
- 'direct_upload' => true,
- 'connection' =>
+ 'remote_directory' => 'uploads',
+ 'direct_upload' => true,
+ 'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
- 'background_upload' => false,
- 'proxy_download' => false } }
+ 'background_upload' => false,
+ 'proxy_download' => false } }
)
allow(Settings).to receive(:[]).with('packages')
.and_return(
{ 'enabled' => true,
- 'object_store' =>
+ 'object_store' =>
{ 'enabled' => false,
- 'remote_directory' => 'packages',
- 'direct_upload' => false,
- 'connection' =>
+ 'remote_directory' => 'packages',
+ 'direct_upload' => false,
+ 'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
'background_upload' => true,
'proxy_download' => false } }
diff --git a/spec/support/matchers/abort_matcher.rb b/spec/support/matchers/abort_matcher.rb
index 64fed2ca069..140953cdc42 100644
--- a/spec/support/matchers/abort_matcher.rb
+++ b/spec/support/matchers/abort_matcher.rb
@@ -13,17 +13,16 @@ RSpec::Matchers.define :abort_execution do
captured = @captured_stderr.string.chomp
@actual_exit_code = e.status
break false unless e.status == 1
-
- if @message
- if @message.is_a? String
- @message == captured
- elsif @message.is_a? Regexp
- @message.match?(captured)
- else
- raise ArgumentError, 'with_message must be either a String or a Regular Expression'
- end
+ break true unless @message
+
+ case @message
+ when String
+ @message == captured
+ when Regexp
+ @message.match?(captured)
+ else
+ raise ArgumentError, 'with_message must be either a String or a Regular Expression'
end
-
ensure
$stderr = original_stderr
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index e6d820104be..db7d4269945 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -230,3 +230,61 @@ RSpec::Matchers.define :expose_permissions_using do |expected|
expect(permission_field.type.of_type.graphql_name).to eq(expected.graphql_name)
end
end
+
+RSpec::Matchers.define :have_graphql_name do |expected|
+ def graphql_name(object)
+ object.graphql_name if object.respond_to?(:graphql_name)
+ end
+
+ match do |object|
+ name = graphql_name(object)
+
+ begin
+ if expected.present?
+ expect(name).to eq(expected)
+ else
+ expect(expected).to be_present
+ end
+ rescue RSpec::Expectations::ExpectationNotMetError => error
+ @error = error
+ raise
+ end
+ end
+
+ failure_message do |object|
+ if expected.present?
+ @error
+ else
+ 'Expected graphql_name value cannot be blank'
+ end
+ end
+end
+
+RSpec::Matchers.define :have_graphql_description do |expected|
+ def graphql_description(object)
+ object.description if object.respond_to?(:description)
+ end
+
+ match do |object|
+ description = graphql_description(object)
+
+ begin
+ if expected.present?
+ expect(description).to eq(expected)
+ else
+ expect(description).to be_present
+ end
+ rescue RSpec::Expectations::ExpectationNotMetError => error
+ @error = error
+ raise
+ end
+ end
+
+ failure_message do |object|
+ if expected.present?
+ @error
+ else
+ "have_graphql_description expected value cannot be blank"
+ end
+ end
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 8bec3be2535..a80c269f915 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -134,7 +134,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-snippet', count: 5)
+ expect(actual).to have_selector('a.gfm.gfm-snippet', count: 9)
end
end
@@ -196,6 +196,16 @@ module MarkdownMatchers
end
end
+ # MathFilter
+ matcher :parse_math do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('[data-math-style="inline"]', count: 4)
+ expect(actual).to have_selector('[data-math-style="display"]', count: 4)
+ end
+ end
+
# InlineDiffFilter
matcher :parse_inline_diffs do
set_default_markdown_messages
diff --git a/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb b/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
index 922f49ba84a..a3cccc3a75d 100644
--- a/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
+++ b/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
@@ -31,14 +31,14 @@ module MigrationHelpers
"links" => [
{
"name" => "Cipher does not check for integrity first?",
- "url" => "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
+ "url" => "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
}
],
"assets" => [
{
"type" => "postman",
"name" => "Test Postman Collection",
- "url" => "http://localhost/test.collection"
+ "url" => "http://localhost/test.collection"
}
],
"evidence" => {
@@ -50,7 +50,7 @@ module MigrationHelpers
"headers" => [
{
"name" => "Accept",
- "value" => "*/*"
+ "value" => "*/*"
}
]
},
@@ -61,7 +61,7 @@ module MigrationHelpers
"headers" => [
{
"name" => "Content-Length",
- "value" => "0"
+ "value" => "0"
}
]
},
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index 421079af8e0..d00d6562966 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+require 'gitlab/redis'
RSpec.configure do |config|
config.after(:each, :redis) do
@@ -7,51 +8,15 @@ RSpec.configure do |config|
end
end
- config.around(:each, :clean_gitlab_redis_cache) do |example|
- redis_cache_cleanup!
+ Gitlab::Redis::ALL_CLASSES.each do |instance_class|
+ underscored_name = instance_class.store_name.underscore
- example.run
+ config.around(:each, :"clean_gitlab_redis_#{underscored_name}") do |example|
+ public_send("redis_#{underscored_name}_cleanup!")
- redis_cache_cleanup!
- end
-
- config.around(:each, :clean_gitlab_redis_shared_state) do |example|
- redis_shared_state_cleanup!
-
- example.run
-
- redis_shared_state_cleanup!
- end
-
- config.around(:each, :clean_gitlab_redis_queues) do |example|
- redis_queues_cleanup!
-
- example.run
-
- redis_queues_cleanup!
- end
-
- config.around(:each, :clean_gitlab_redis_trace_chunks) do |example|
- redis_trace_chunks_cleanup!
+ example.run
- example.run
-
- redis_trace_chunks_cleanup!
- end
-
- config.around(:each, :clean_gitlab_redis_rate_limiting) do |example|
- redis_rate_limiting_cleanup!
-
- example.run
-
- redis_rate_limiting_cleanup!
- end
-
- config.around(:each, :clean_gitlab_redis_sessions) do |example|
- redis_sessions_cleanup!
-
- example.run
-
- redis_sessions_cleanup!
+ public_send("redis_#{underscored_name}_cleanup!")
+ end
end
end
diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb
index 90c15dea1f8..34ac69236ee 100644
--- a/spec/support/redis/redis_helpers.rb
+++ b/spec/support/redis/redis_helpers.rb
@@ -1,36 +1,10 @@
# frozen_string_literal: true
module RedisHelpers
- # config/README.md
-
- # Usage: performance enhancement
- def redis_cache_cleanup!
- Gitlab::Redis::Cache.with(&:flushdb)
- end
-
- # Usage: SideKiq, Mailroom, CI Runner, Workhorse, push services
- def redis_queues_cleanup!
- Gitlab::Redis::Queues.with(&:flushdb)
- end
-
- # Usage: session state, rate limiting
- def redis_shared_state_cleanup!
- Gitlab::Redis::SharedState.with(&:flushdb)
- end
-
- # Usage: CI trace chunks
- def redis_trace_chunks_cleanup!
- Gitlab::Redis::TraceChunks.with(&:flushdb)
- end
-
- # Usage: rate limiting state (for Rack::Attack)
- def redis_rate_limiting_cleanup!
- Gitlab::Redis::RateLimiting.with(&:flushdb)
- end
-
- # Usage: session state
- def redis_sessions_cleanup!
- Gitlab::Redis::Sessions.with(&:flushdb)
+ Gitlab::Redis::ALL_CLASSES.each do |instance_class|
+ define_method("redis_#{instance_class.store_name.underscore}_cleanup!") do
+ instance_class.with(&:flushdb)
+ end
end
# Usage: reset cached instance config
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index d4c8682ec71..33945509675 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -3,19 +3,19 @@
RSpec.shared_examples "redis_shared_examples" do
include StubENV
- let(:test_redis_url) { "redis://redishost:#{redis_port}"}
+ let(:test_redis_url) { "redis://redishost:#{redis_port}" }
let(:config_file_name) { instance_specific_config_file }
let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
- let(:old_socket_path) {"/path/to/old/redis.sock" }
- let(:new_socket_path) {"/path/to/redis.sock" }
+ let(:old_socket_path) { "/path/to/old/redis.sock" }
+ let(:new_socket_path) { "/path/to/redis.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
let(:redis_port) { 6379 }
let(:redis_database) { 99 }
let(:sentinel_port) { 26379 }
- let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml"}
- let(:config_env_variable_url) {"TEST_GITLAB_REDIS_URL"}
+ let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml" }
+ let(:config_env_variable_url) { "TEST_GITLAB_REDIS_URL" }
let(:rails_root) { Dir.mktmpdir('redis_shared_examples') }
before do
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 30e48b3baf1..6795d2f6d2a 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -1,22 +1,26 @@
# frozen_string_literal: true
+require_relative "rspec_order"
+require_relative "system_exit_detected"
require_relative "helpers/stub_configuration"
require_relative "helpers/stub_metrics"
require_relative "helpers/stub_object_storage"
require_relative "helpers/stub_env"
require_relative "helpers/fast_rails_root"
-# so we need to load rubocop here due to the rubocop support file loading cop_helper
-# which monkey patches class Cop
-# if cop helper is loaded before rubocop (where class Cop is defined as class Cop < Base)
-# we get a `superclass mismatch for class Cop` error when running a rspec for a locally defined
-# rubocop cop - therefore we need rubocop required first since it had an inheritance added to the Cop class
-require 'rubocop'
-require 'rubocop/rspec/support'
-
RSpec::Expectations.configuration.on_potential_false_positives = :raise
RSpec.configure do |config|
+ # Re-run failures locally with `--only-failures`
+ config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt')
+
+ unless ENV['CI']
+ # Allow running `:focus` examples locally,
+ # falling back to all tests when there is no `:focus` example.
+ config.filter_run focus: true
+ config.run_all_when_everything_filtered = true
+ end
+
config.mock_with :rspec do |mocks|
mocks.verify_doubled_constant_names = true
end
@@ -28,10 +32,4 @@ RSpec.configure do |config|
config.include StubObjectStorage
config.include StubENV
config.include FastRailsRoot
-
- config.include RuboCop::RSpec::ExpectOffense, type: :rubocop
-
- config.define_derived_metadata(file_path: %r{spec/rubocop}) do |metadata|
- metadata[:type] = :rubocop
- end
end
diff --git a/spec/support/rspec_order.rb b/spec/support/rspec_order.rb
new file mode 100644
index 00000000000..c128e18b38e
--- /dev/null
+++ b/spec/support/rspec_order.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Support
+ module RspecOrder
+ TODO_YAML = File.join(__dir__, 'rspec_order_todo.yml')
+
+ module_function
+
+ def order_for(example_group)
+ order_from_env || random_order(example_group)
+ end
+
+ def order_from_env
+ return @order_from_env if defined?(@order_from_env)
+
+ # Passing custom defined order via `--order NAME` is not supported.
+ # For example, `--order reverse` does not work so we are passing it via
+ # environment variable RSPEC_ORDER.
+ @order_from_env = ENV['RSPEC_ORDER']
+ end
+
+ def random_order(example_group)
+ path = example_group.metadata.fetch(:file_path)
+
+ :random unless potential_order_dependent?(path)
+ end
+
+ def potential_order_dependent?(path)
+ @todo ||= YAML.load_file(TODO_YAML).to_set # rubocop:disable Gitlab/PredicateMemoization
+
+ @todo.include?(path)
+ end
+
+ # Adds '# order <ORDER>` below the example group description if the order
+ # has been set to help debugging in case of failure.
+ #
+ # Previously, we've modified metadata[:description] directly but that led
+ # to bugs. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96137
+ module DocumentationFormatterPatch
+ # See https://github.com/rspec/rspec-core/blob/v3.11.0/lib/rspec/core/formatters/documentation_formatter.rb#L24-L29
+ def example_group_started(notification)
+ super
+
+ order = notification.group.metadata[:order]
+ return unless order
+
+ output.puts "#{current_indentation}# order #{order}"
+ end
+ end
+ end
+end
+
+RSpec::Core::Formatters::DocumentationFormatter.prepend Support::RspecOrder::DocumentationFormatterPatch
+
+RSpec.configure do |config|
+ # Useful to find order-dependent specs.
+ config.register_ordering(:reverse, &:reverse)
+
+ # Randomization can be reproduced across test runs.
+ Kernel.srand config.seed
+
+ config.on_example_group_definition do |example_group|
+ order = Support::RspecOrder.order_for(example_group)
+
+ example_group.metadata[:order] = order.to_sym if order
+ end
+end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
new file mode 100644
index 00000000000..b5e3d707d50
--- /dev/null
+++ b/spec/support/rspec_order_todo.yml
@@ -0,0 +1,11150 @@
+# The following specs are excluded from running in random order.
+# They are run in defined order.
+#
+# See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-order.
+#
+---
+- './ee/spec/components/billing/plan_component_spec.rb'
+- './ee/spec/components/namespaces/free_user_cap/alert_component_spec.rb'
+- './ee/spec/components/namespaces/free_user_cap/preview_alert_component_spec.rb'
+- './ee/spec/components/namespaces/free_user_cap/preview_usage_quota_alert_component_spec.rb'
+- './ee/spec/components/namespaces/free_user_cap/usage_quota_alert_component_spec.rb'
+- './ee/spec/components/namespaces/free_user_cap/usage_quota_trial_alert_component_spec.rb'
+- './ee/spec/components/namespaces/storage/limit_alert_component_spec.rb'
+- './ee/spec/config/metrics/every_metric_definition_spec.rb'
+- './ee/spec/controllers/admin/applications_controller_spec.rb'
+- './ee/spec/controllers/admin/application_settings_controller_spec.rb'
+- './ee/spec/controllers/admin/audit_log_reports_controller_spec.rb'
+- './ee/spec/controllers/admin/audit_logs_controller_spec.rb'
+- './ee/spec/controllers/admin/clusters_controller_spec.rb'
+- './ee/spec/controllers/admin/dashboard_controller_spec.rb'
+- './ee/spec/controllers/admin/dev_ops_report_controller_spec.rb'
+- './ee/spec/controllers/admin/elasticsearch_controller_spec.rb'
+- './ee/spec/controllers/admin/emails_controller_spec.rb'
+- './ee/spec/controllers/admin/geo/nodes_controller_spec.rb'
+- './ee/spec/controllers/admin/geo/projects_controller_spec.rb'
+- './ee/spec/controllers/admin/geo/settings_controller_spec.rb'
+- './ee/spec/controllers/admin/groups_controller_spec.rb'
+- './ee/spec/controllers/admin/impersonations_controller_spec.rb'
+- './ee/spec/controllers/admin/licenses_controller_spec.rb'
+- './ee/spec/controllers/admin/licenses/usage_exports_controller_spec.rb'
+- './ee/spec/controllers/admin/projects_controller_spec.rb'
+- './ee/spec/controllers/admin/push_rules_controller_spec.rb'
+- './ee/spec/controllers/admin/runners_controller_spec.rb'
+- './ee/spec/controllers/admin/users_controller_spec.rb'
+- './ee/spec/controllers/autocomplete_controller_spec.rb'
+- './ee/spec/controllers/boards/issues_controller_spec.rb'
+- './ee/spec/controllers/boards/lists_controller_spec.rb'
+- './ee/spec/controllers/boards/milestones_controller_spec.rb'
+- './ee/spec/controllers/boards/users_controller_spec.rb'
+- './ee/spec/controllers/concerns/boards_responses_spec.rb'
+- './ee/spec/controllers/concerns/ee/routable_actions/sso_enforcement_redirect_spec.rb'
+- './ee/spec/controllers/concerns/geo_instrumentation_spec.rb'
+- './ee/spec/controllers/concerns/gitlab_subscriptions/seat_count_alert_spec.rb'
+- './ee/spec/controllers/concerns/internal_redirect_spec.rb'
+- './ee/spec/controllers/concerns/registrations/verification_spec.rb'
+- './ee/spec/controllers/concerns/routable_actions_spec.rb'
+- './ee/spec/controllers/countries_controller_spec.rb'
+- './ee/spec/controllers/country_states_controller_spec.rb'
+- './ee/spec/controllers/dashboard_controller_spec.rb'
+- './ee/spec/controllers/ee/admin/sessions_controller_spec.rb'
+- './ee/spec/controllers/ee/dashboard/projects_controller_spec.rb'
+- './ee/spec/controllers/ee/groups_controller_spec.rb'
+- './ee/spec/controllers/ee/groups/variables_controller_spec.rb'
+- './ee/spec/controllers/ee/omniauth_callbacks_controller_spec.rb'
+- './ee/spec/controllers/ee/profiles/preferences_controller_spec.rb'
+- './ee/spec/controllers/ee/projects/autocomplete_sources_controller_spec.rb'
+- './ee/spec/controllers/ee/projects/blob_controller_spec.rb'
+- './ee/spec/controllers/ee/projects/jobs_controller_spec.rb'
+- './ee/spec/controllers/ee/projects/merge_requests/content_controller_spec.rb'
+- './ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb'
+- './ee/spec/controllers/ee/projects/variables_controller_spec.rb'
+- './ee/spec/controllers/ee/registrations_controller_spec.rb'
+- './ee/spec/controllers/ee/root_controller_spec.rb'
+- './ee/spec/controllers/ee/search_controller_spec.rb'
+- './ee/spec/controllers/ee/sent_notifications_controller_spec.rb'
+- './ee/spec/controllers/ee/sessions_controller_spec.rb'
+- './ee/spec/controllers/ee/uploads_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/ci_cd_analytics_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/coverage_reports_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/cycle_analytics_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/cycle_analytics/stages_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/cycle_analytics/value_streams_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/productivity_analytics_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/repository_analytics_controller_spec.rb'
+- './ee/spec/controllers/groups/analytics/tasks_by_type_controller_spec.rb'
+- './ee/spec/controllers/groups/audit_events_controller_spec.rb'
+- './ee/spec/controllers/groups/billings_controller_spec.rb'
+- './ee/spec/controllers/groups/boards_controller_spec.rb'
+- './ee/spec/controllers/groups/clusters_controller_spec.rb'
+- './ee/spec/controllers/groups/contribution_analytics_controller_spec.rb'
+- './ee/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb'
+- './ee/spec/controllers/groups/epic_boards_controller_spec.rb'
+- './ee/spec/controllers/groups/epic_issues_controller_spec.rb'
+- './ee/spec/controllers/groups/epics_controller_spec.rb'
+- './ee/spec/controllers/groups/epics/notes_controller_spec.rb'
+- './ee/spec/controllers/groups/group_members_controller_spec.rb'
+- './ee/spec/controllers/groups/groups_controller_spec.rb'
+- './ee/spec/controllers/groups/hooks_controller_spec.rb'
+- './ee/spec/controllers/groups/insights_controller_spec.rb'
+- './ee/spec/controllers/groups/issues_analytics_controller_spec.rb'
+- './ee/spec/controllers/groups/issues_controller_spec.rb'
+- './ee/spec/controllers/groups/iteration_cadences_controller_spec.rb'
+- './ee/spec/controllers/groups/iterations_controller_spec.rb'
+- './ee/spec/controllers/groups/ldaps_controller_spec.rb'
+- './ee/spec/controllers/groups/ldap_settings_controller_spec.rb'
+- './ee/spec/controllers/groups/merge_requests_controller_spec.rb'
+- './ee/spec/controllers/groups/omniauth_callbacks_controller_spec.rb'
+- './ee/spec/controllers/groups/push_rules_controller_spec.rb'
+- './ee/spec/controllers/groups/roadmap_controller_spec.rb'
+- './ee/spec/controllers/groups/runners_controller_spec.rb'
+- './ee/spec/controllers/groups/saml_group_links_controller_spec.rb'
+- './ee/spec/controllers/groups/saml_providers_controller_spec.rb'
+- './ee/spec/controllers/groups/scim_oauth_controller_spec.rb'
+- './ee/spec/controllers/groups/seat_usage_controller_spec.rb'
+- './ee/spec/controllers/groups/security/compliance_dashboards_controller_spec.rb'
+- './ee/spec/controllers/groups/security/dashboard_controller_spec.rb'
+- './ee/spec/controllers/groups/security/merge_commit_reports_controller_spec.rb'
+- './ee/spec/controllers/groups/security/policies_controller_spec.rb'
+- './ee/spec/controllers/groups/security/vulnerabilities_controller_spec.rb'
+- './ee/spec/controllers/groups/sso_controller_spec.rb'
+- './ee/spec/controllers/groups/todos_controller_spec.rb'
+- './ee/spec/controllers/groups/usage_quotas_controller_spec.rb'
+- './ee/spec/controllers/groups/wikis_controller_spec.rb'
+- './ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb'
+- './ee/spec/controllers/oauth/applications_controller_spec.rb'
+- './ee/spec/controllers/oauth/geo_auth_controller_spec.rb'
+- './ee/spec/controllers/operations_controller_spec.rb'
+- './ee/spec/controllers/passwords_controller_spec.rb'
+- './ee/spec/controllers/profiles/billings_controller_spec.rb'
+- './ee/spec/controllers/profiles_controller_spec.rb'
+- './ee/spec/controllers/profiles/keys_controller_spec.rb'
+- './ee/spec/controllers/profiles/slacks_controller_spec.rb'
+- './ee/spec/controllers/profiles/usage_quotas_controller_spec.rb'
+- './ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb'
+- './ee/spec/controllers/projects/analytics/issues_analytics_controller_spec.rb'
+- './ee/spec/controllers/projects/analytics/merge_request_analytics_controller_spec.rb'
+- './ee/spec/controllers/projects/approver_groups_controller_spec.rb'
+- './ee/spec/controllers/projects/approvers_controller_spec.rb'
+- './ee/spec/controllers/projects/audit_events_controller_spec.rb'
+- './ee/spec/controllers/projects/boards_controller_spec.rb'
+- './ee/spec/controllers/projects/branches_controller_spec.rb'
+- './ee/spec/controllers/projects/clusters_controller_spec.rb'
+- './ee/spec/controllers/projects_controller_spec.rb'
+- './ee/spec/controllers/projects/dependencies_controller_spec.rb'
+- './ee/spec/controllers/projects/deploy_keys_controller_spec.rb'
+- './ee/spec/controllers/projects/environments_controller_spec.rb'
+- './ee/spec/controllers/projects/feature_flag_issues_controller_spec.rb'
+- './ee/spec/controllers/projects/imports_controller_spec.rb'
+- './ee/spec/controllers/projects/incident_management/escalation_policies_controller_spec.rb'
+- './ee/spec/controllers/projects/incident_management/oncall_schedules_controller_spec.rb'
+- './ee/spec/controllers/projects/insights_controller_spec.rb'
+- './ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb'
+- './ee/spec/controllers/projects/integrations/zentao/issues_controller_spec.rb'
+- './ee/spec/controllers/projects/issue_links_controller_spec.rb'
+- './ee/spec/controllers/projects/issues_controller_spec.rb'
+- './ee/spec/controllers/projects/iteration_cadences_controller_spec.rb'
+- './ee/spec/controllers/projects/iterations_controller_spec.rb'
+- './ee/spec/controllers/projects/legacy_pipelines_controller_spec.rb'
+- './ee/spec/controllers/projects/licenses_controller_spec.rb'
+- './ee/spec/controllers/projects/merge_requests_controller_spec.rb'
+- './ee/spec/controllers/projects/merge_requests/creations_controller_spec.rb'
+- './ee/spec/controllers/projects/mirrors_controller_spec.rb'
+- './ee/spec/controllers/projects/pages_controller_spec.rb'
+- './ee/spec/controllers/projects/path_locks_controller_spec.rb'
+- './ee/spec/controllers/projects/pipelines_controller_spec.rb'
+- './ee/spec/controllers/projects/protected_environments_controller_spec.rb'
+- './ee/spec/controllers/projects/push_rules_controller_spec.rb'
+- './ee/spec/controllers/projects/quality/test_cases_controller_spec.rb'
+- './ee/spec/controllers/projects/repositories_controller_spec.rb'
+- './ee/spec/controllers/projects/requirements_management/requirements_controller_spec.rb'
+- './ee/spec/controllers/projects/runners_controller_spec.rb'
+- './ee/spec/controllers/projects/security/api_fuzzing_configuration_controller_spec.rb'
+- './ee/spec/controllers/projects/security/configuration_controller_spec.rb'
+- './ee/spec/controllers/projects/security/dashboard_controller_spec.rb'
+- './ee/spec/controllers/projects/security/sast_configuration_controller_spec.rb'
+- './ee/spec/controllers/projects/security/scanned_resources_controller_spec.rb'
+- './ee/spec/controllers/projects/security/vulnerabilities_controller_spec.rb'
+- './ee/spec/controllers/projects/security/vulnerabilities/notes_controller_spec.rb'
+- './ee/spec/controllers/projects/security/vulnerability_report_controller_spec.rb'
+- './ee/spec/controllers/projects/settings/ci_cd_controller_spec.rb'
+- './ee/spec/controllers/projects/settings/integrations_controller_spec.rb'
+- './ee/spec/controllers/projects/settings/operations_controller_spec.rb'
+- './ee/spec/controllers/projects/settings/repository_controller_spec.rb'
+- './ee/spec/controllers/projects/settings/slacks_controller_spec.rb'
+- './ee/spec/controllers/projects/subscriptions_controller_spec.rb'
+- './ee/spec/controllers/projects/vulnerability_feedback_controller_spec.rb'
+- './ee/spec/controllers/registrations/company_controller_spec.rb'
+- './ee/spec/controllers/registrations/groups_controller_spec.rb'
+- './ee/spec/controllers/registrations/groups_projects_controller_spec.rb'
+- './ee/spec/controllers/registrations/projects_controller_spec.rb'
+- './ee/spec/controllers/registrations/verification_controller_spec.rb'
+- './ee/spec/controllers/registrations/welcome_controller_spec.rb'
+- './ee/spec/controllers/repositories/git_http_controller_spec.rb'
+- './ee/spec/controllers/security/dashboard_controller_spec.rb'
+- './ee/spec/controllers/security/projects_controller_spec.rb'
+- './ee/spec/controllers/security/vulnerabilities_controller_spec.rb'
+- './ee/spec/controllers/sitemap_controller_spec.rb'
+- './ee/spec/controllers/subscriptions_controller_spec.rb'
+- './ee/spec/controllers/subscriptions/groups_controller_spec.rb'
+- './ee/spec/controllers/trial_registrations_controller_spec.rb'
+- './ee/spec/controllers/trials_controller_spec.rb'
+- './ee/spec/controllers/users_controller_spec.rb'
+- './ee/spec/db/production/license_spec.rb'
+- './ee/spec/elastic_integration/global_search_spec.rb'
+- './ee/spec/elastic_integration/repository_index_spec.rb'
+- './ee/spec/elastic/migrate/20201105181100_apply_max_analyzed_offset_spec.rb'
+- './ee/spec/elastic/migrate/20201116142400_add_new_data_to_issues_documents_spec.rb'
+- './ee/spec/elastic/migrate/20201123123400_migrate_issues_to_separate_index_spec.rb'
+- './ee/spec/elastic/migrate/20210112165500_delete_issues_from_original_index_spec.rb'
+- './ee/spec/elastic/migrate/20210127154600_remove_permissions_data_from_notes_documents_spec.rb'
+- './ee/spec/elastic/migrate/20210128163600_add_permissions_data_to_notes_documents_spec.rb'
+- './ee/spec/elastic/migrate/20210201104800_migrate_notes_to_separate_index_spec.rb'
+- './ee/spec/elastic/migrate/20210421140400_add_new_data_to_merge_requests_documents_spec.rb'
+- './ee/spec/elastic/migrate/20210429154500_migrate_merge_requests_to_separate_index_spec.rb'
+- './ee/spec/elastic/migrate/20210510113500_delete_merge_requests_from_original_index_spec.rb'
+- './ee/spec/elastic/migrate/20210510143200_delete_notes_from_original_index_spec.rb'
+- './ee/spec/elastic/migrate/20210623081800_add_upvotes_to_issues_spec.rb'
+- './ee/spec/elastic/migrate/20210722112500_add_upvotes_mappings_to_merge_requests_spec.rb'
+- './ee/spec/elastic/migrate/20210813134600_add_namespace_ancestry_to_issues_mapping_spec.rb'
+- './ee/spec/elastic/migrate/20210825110300_backfill_namespace_ancestry_for_issues_spec.rb'
+- './ee/spec/elastic/migrate/20210910094600_add_namespace_ancestry_ids_to_issues_mapping_spec.rb'
+- './ee/spec/elastic/migrate/20210910100000_redo_backfill_namespace_ancestry_ids_for_issues_spec.rb'
+- './ee/spec/elastic/migrate/20220118150500_delete_orphaned_commits_spec.rb'
+- './ee/spec/elastic/migrate/20220119120500_populate_commit_permissions_in_main_index_spec.rb'
+- './ee/spec/elastic/migrate/20220512150000_pause_indexing_for_unsupported_es_versions_spec.rb'
+- './ee/spec/elastic/migrate/20220613120500_migrate_commits_to_separate_index_spec.rb'
+- './ee/spec/elastic/migrate/20220713103500_delete_commits_from_original_index_spec.rb'
+- './ee/spec/factories/lfs_object_spec.rb'
+- './ee/spec/features/account_recovery_regular_check_spec.rb'
+- './ee/spec/features/admin/admin_credentials_inventory_spec.rb'
+- './ee/spec/features/admin/admin_dashboard_spec.rb'
+- './ee/spec/features/admin/admin_dev_ops_reports_spec.rb'
+- './ee/spec/features/admin/admin_emails_spec.rb'
+- './ee/spec/features/admin/admin_groups_spec.rb'
+- './ee/spec/features/admin/admin_interacts_with_push_rules_spec.rb'
+- './ee/spec/features/admin/admin_merge_requests_approvals_spec.rb'
+- './ee/spec/features/admin/admin_reset_pipeline_minutes_spec.rb'
+- './ee/spec/features/admin/admin_runners_spec.rb'
+- './ee/spec/features/admin/admin_sends_notification_spec.rb'
+- './ee/spec/features/admin/admin_settings_spec.rb'
+- './ee/spec/features/admin/admin_show_new_user_signups_cap_alert_spec.rb'
+- './ee/spec/features/admin/admin_users_spec.rb'
+- './ee/spec/features/admin/geo/admin_geo_nodes_spec.rb'
+- './ee/spec/features/admin/geo/admin_geo_projects_spec.rb'
+- './ee/spec/features/admin/geo/admin_geo_replication_nav_spec.rb'
+- './ee/spec/features/admin/geo/admin_geo_sidebar_spec.rb'
+- './ee/spec/features/admin/groups/admin_changes_plan_spec.rb'
+- './ee/spec/features/admin/groups/admin_subscription_alerts_spec.rb'
+- './ee/spec/features/admin/licenses/admin_adds_license_spec.rb'
+- './ee/spec/features/admin/licenses/show_user_count_threshold_spec.rb'
+- './ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb'
+- './ee/spec/features/admin/users/users_spec.rb'
+- './ee/spec/features/analytics/code_analytics_spec.rb'
+- './ee/spec/features/analytics/group_analytics_spec.rb'
+- './ee/spec/features/billings/billing_plans_spec.rb'
+- './ee/spec/features/billings/extend_reactivate_trial_spec.rb'
+- './ee/spec/features/billings/qrtly_reconciliation_alert_spec.rb'
+- './ee/spec/features/boards/board_filters_spec.rb'
+- './ee/spec/features/boards/boards_licensed_features_spec.rb'
+- './ee/spec/features/boards/boards_spec.rb'
+- './ee/spec/features/boards/group_boards/board_deletion_spec.rb'
+- './ee/spec/features/boards/group_boards/multiple_boards_spec.rb'
+- './ee/spec/features/boards/new_issue_spec.rb'
+- './ee/spec/features/boards/scoped_issue_board_spec.rb'
+- './ee/spec/features/boards/sidebar_spec.rb'
+- './ee/spec/features/boards/swimlanes/epics_swimlanes_drag_drop_spec.rb'
+- './ee/spec/features/boards/swimlanes/epics_swimlanes_filtering_spec.rb'
+- './ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_labels_spec.rb'
+- './ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_spec.rb'
+- './ee/spec/features/boards/swimlanes/epics_swimlanes_spec.rb'
+- './ee/spec/features/boards/user_adds_lists_to_board_spec.rb'
+- './ee/spec/features/boards/user_visits_board_spec.rb'
+- './ee/spec/features/burndown_charts_spec.rb'
+- './ee/spec/features/burnup_charts_spec.rb'
+- './ee/spec/features/ci/ci_minutes_spec.rb'
+- './ee/spec/features/ci_shared_runner_settings_spec.rb'
+- './ee/spec/features/ci_shared_runner_warnings_spec.rb'
+- './ee/spec/features/clusters/cluster_detail_page_spec.rb'
+- './ee/spec/features/contextual_sidebar_spec.rb'
+- './ee/spec/features/dashboards/activity_spec.rb'
+- './ee/spec/features/dashboards/groups_spec.rb'
+- './ee/spec/features/dashboards/issues_spec.rb'
+- './ee/spec/features/dashboards/merge_requests_spec.rb'
+- './ee/spec/features/dashboards/operations_spec.rb'
+- './ee/spec/features/dashboards/projects_spec.rb'
+- './ee/spec/features/dashboards/todos_spec.rb'
+- './ee/spec/features/discussion_comments/epic_quick_actions_spec.rb'
+- './ee/spec/features/discussion_comments/epic_spec.rb'
+- './ee/spec/features/epic_boards/epic_boards_sidebar_spec.rb'
+- './ee/spec/features/epic_boards/epic_boards_spec.rb'
+- './ee/spec/features/epic_boards/multiple_epic_boards_spec.rb'
+- './ee/spec/features/epic_boards/new_epic_spec.rb'
+- './ee/spec/features/epics/delete_epic_spec.rb'
+- './ee/spec/features/epics/epic_issues_spec.rb'
+- './ee/spec/features/epics/epic_labels_spec.rb'
+- './ee/spec/features/epics/epic_related_epics_spec.rb'
+- './ee/spec/features/epics/epic_show_spec.rb'
+- './ee/spec/features/epics/epics_list_spec.rb'
+- './ee/spec/features/epics/gfm_autocomplete_spec.rb'
+- './ee/spec/features/epics/issue_promotion_spec.rb'
+- './ee/spec/features/epics/referencing_epics_spec.rb'
+- './ee/spec/features/epics/shortcuts_epic_spec.rb'
+- './ee/spec/features/epics/todo_spec.rb'
+- './ee/spec/features/epics/update_epic_spec.rb'
+- './ee/spec/features/epics/user_uses_quick_actions_spec.rb'
+- './ee/spec/features/geo_node_spec.rb'
+- './ee/spec/features/gitlab_subscriptions/seat_count_alert_spec.rb'
+- './ee/spec/features/google_analytics_datalayer_spec.rb'
+- './ee/spec/features/groups/active_tabs_spec.rb'
+- './ee/spec/features/groups/analytics/ci_cd_analytics_spec.rb'
+- './ee/spec/features/groups/analytics/cycle_analytics/charts_spec.rb'
+- './ee/spec/features/groups/analytics/cycle_analytics/filters_and_data_spec.rb'
+- './ee/spec/features/groups/analytics/cycle_analytics/multiple_value_streams_spec.rb'
+- './ee/spec/features/groups/analytics/productivity_analytics_spec.rb'
+- './ee/spec/features/groups/audit_events_spec.rb'
+- './ee/spec/features/groups/billing_spec.rb'
+- './ee/spec/features/groups/contribution_analytics_spec.rb'
+- './ee/spec/features/groups/feature_discovery_moments_spec.rb'
+- './ee/spec/features/groups/group_overview_spec.rb'
+- './ee/spec/features/groups/group_page_with_external_authorization_service_spec.rb'
+- './ee/spec/features/groups/group_projects_spec.rb'
+- './ee/spec/features/groups/group_roadmap_spec.rb'
+- './ee/spec/features/groups/group_settings_spec.rb'
+- './ee/spec/features/groups/groups_security_credentials_spec.rb'
+- './ee/spec/features/groups/hooks/user_adds_hook_spec.rb'
+- './ee/spec/features/groups/hooks/user_edits_hooks_spec.rb'
+- './ee/spec/features/groups/hooks/user_tests_hooks_spec.rb'
+- './ee/spec/features/groups/hooks/user_views_hooks_spec.rb'
+- './ee/spec/features/groups/insights_spec.rb'
+- './ee/spec/features/groups/issues_spec.rb'
+- './ee/spec/features/groups/iterations/iterations_list_spec.rb'
+- './ee/spec/features/groups/iteration_spec.rb'
+- './ee/spec/features/groups/iterations/user_creates_iteration_in_cadence_spec.rb'
+- './ee/spec/features/groups/iterations/user_edits_iteration_cadence_spec.rb'
+- './ee/spec/features/groups/iterations/user_edits_iteration_spec.rb'
+- './ee/spec/features/groups/iterations/user_views_iteration_cadence_spec.rb'
+- './ee/spec/features/groups/iterations/user_views_iteration_spec.rb'
+- './ee/spec/features/groups/ldap_group_links_spec.rb'
+- './ee/spec/features/groups/ldap_settings_spec.rb'
+- './ee/spec/features/groups/members/leave_group_spec.rb'
+- './ee/spec/features/groups/members/list_members_spec.rb'
+- './ee/spec/features/groups/members/manage_groups_spec.rb'
+- './ee/spec/features/groups/members/manage_members_spec.rb'
+- './ee/spec/features/groups/members/override_ldap_memberships_spec.rb'
+- './ee/spec/features/groups/navbar_spec.rb'
+- './ee/spec/features/groups/new_spec.rb'
+- './ee/spec/features/groups/push_rules_spec.rb'
+- './ee/spec/features/groups/saml_enforcement_spec.rb'
+- './ee/spec/features/groups/saml_group_links_spec.rb'
+- './ee/spec/features/groups/saml_providers_spec.rb'
+- './ee/spec/features/groups/scim_token_spec.rb'
+- './ee/spec/features/groups/seat_usage/seat_usage_spec.rb'
+- './ee/spec/features/groups/security/compliance_dashboards_spec.rb'
+- './ee/spec/features/groups/settings/ci_cd_spec.rb'
+- './ee/spec/features/groups/settings/protected_environments_spec.rb'
+- './ee/spec/features/groups/settings/reporting_spec.rb'
+- './ee/spec/features/groups/settings/user_configures_insights_spec.rb'
+- './ee/spec/features/groups/settings/user_searches_in_settings_spec.rb'
+- './ee/spec/features/groups_spec.rb'
+- './ee/spec/features/groups/sso_spec.rb'
+- './ee/spec/features/groups/usage_quotas_spec.rb'
+- './ee/spec/features/groups/wikis_spec.rb'
+- './ee/spec/features/groups/wiki/user_views_wiki_empty_spec.rb'
+- './ee/spec/features/ide/user_commits_changes_spec.rb'
+- './ee/spec/features/ide/user_opens_ide_spec.rb'
+- './ee/spec/features/incidents/incident_details_spec.rb'
+- './ee/spec/features/incidents/incidents_list_spec.rb'
+- './ee/spec/features/integrations/jira/jira_issues_list_spec.rb'
+- './ee/spec/features/invites_spec.rb'
+- './ee/spec/features/issues/blocking_issues_spec.rb'
+- './ee/spec/features/issues/epic_in_issue_sidebar_spec.rb'
+- './ee/spec/features/issues/filtered_search/filter_issues_by_iteration_spec.rb'
+- './ee/spec/features/issues/filtered_search/filter_issues_by_multiple_assignees_spec.rb'
+- './ee/spec/features/issues/filtered_search/filter_issues_epic_spec.rb'
+- './ee/spec/features/issues/filtered_search/filter_issues_weight_spec.rb'
+- './ee/spec/features/issues/form_spec.rb'
+- './ee/spec/features/issues/gfm_autocomplete_ee_spec.rb'
+- './ee/spec/features/issues/issue_actions_spec.rb'
+- './ee/spec/features/issues/issue_sidebar_spec.rb'
+- './ee/spec/features/issues/move_issue_resource_weight_events_spec.rb'
+- './ee/spec/features/issues/related_issues_spec.rb'
+- './ee/spec/features/issues/resource_weight_events_spec.rb'
+- './ee/spec/features/issues/sub_nav_ee_spec.rb'
+- './ee/spec/features/issues/user_bulk_edits_issues_spec.rb'
+- './ee/spec/features/issues/user_edits_issue_spec.rb'
+- './ee/spec/features/issues/user_sees_empty_state_spec.rb'
+- './ee/spec/features/issues/user_uses_quick_actions_spec.rb'
+- './ee/spec/features/issues/user_views_issues_spec.rb'
+- './ee/spec/features/labels_hierarchy_spec.rb'
+- './ee/spec/features/markdown/markdown_spec.rb'
+- './ee/spec/features/markdown/metrics_spec.rb'
+- './ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb'
+- './ee/spec/features/merge_request/sidebar_spec.rb'
+- './ee/spec/features/merge_requests/user_filters_by_approvers_spec.rb'
+- './ee/spec/features/merge_requests/user_resets_approvers_spec.rb'
+- './ee/spec/features/merge_requests/user_views_all_merge_requests_spec.rb'
+- './ee/spec/features/merge_request/user_approves_spec.rb'
+- './ee/spec/features/merge_request/user_approves_with_password_spec.rb'
+- './ee/spec/features/merge_request/user_comments_on_merge_request_spec.rb'
+- './ee/spec/features/merge_request/user_creates_merge_request_spec.rb'
+- './ee/spec/features/merge_request/user_creates_merge_request_with_blocking_mrs_spec.rb'
+- './ee/spec/features/merge_request/user_creates_multiple_assignees_mr_spec.rb'
+- './ee/spec/features/merge_request/user_creates_multiple_reviewers_mr_spec.rb'
+- './ee/spec/features/merge_request/user_edits_approval_rules_mr_spec.rb'
+- './ee/spec/features/merge_request/user_edits_merge_request_blocking_mrs_spec.rb'
+- './ee/spec/features/merge_request/user_edits_multiple_assignees_mr_spec.rb'
+- './ee/spec/features/merge_request/user_edits_multiple_reviewers_mr_spec.rb'
+- './ee/spec/features/merge_request/user_merges_immediately_spec.rb'
+- './ee/spec/features/merge_request/user_merges_with_namespace_storage_limits_spec.rb'
+- './ee/spec/features/merge_request/user_merges_with_push_rules_spec.rb'
+- './ee/spec/features/merge_request/user_sees_approval_widget_spec.rb'
+- './ee/spec/features/merge_request/user_sees_closing_issues_message_spec.rb'
+- './ee/spec/features/merge_request/user_sees_merge_widget_spec.rb'
+- './ee/spec/features/merge_request/user_sees_mr_approvals_promo_spec.rb'
+- './ee/spec/features/merge_request/user_sees_status_checks_widget_spec.rb'
+- './ee/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb'
+- './ee/spec/features/merge_request/user_sets_approval_rules_spec.rb'
+- './ee/spec/features/merge_request/user_sets_approvers_spec.rb'
+- './ee/spec/features/merge_request/user_uses_slash_commands_spec.rb'
+- './ee/spec/features/merge_request/user_views_blocked_merge_request_spec.rb'
+- './ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb'
+- './ee/spec/features/merge_trains/user_adds_merge_request_to_merge_train_spec.rb'
+- './ee/spec/features/merge_trains/user_adds_to_merge_train_when_pipeline_succeeds_spec.rb'
+- './ee/spec/features/milestones/user_views_milestone_spec.rb'
+- './ee/spec/features/namespace_user_cap_reached_alert_spec.rb'
+- './ee/spec/features/oncall_schedules/user_creates_schedule_spec.rb'
+- './ee/spec/features/operations_nav_link_spec.rb'
+- './ee/spec/features/password_reset_spec.rb'
+- './ee/spec/features/pending_group_memberships_spec.rb'
+- './ee/spec/features/pending_project_memberships_spec.rb'
+- './ee/spec/features/profiles/account_spec.rb'
+- './ee/spec/features/profiles/billing_spec.rb'
+- './ee/spec/features/profiles/password_spec.rb'
+- './ee/spec/features/profiles/usage_quotas_spec.rb'
+- './ee/spec/features/profiles/user_visits_public_profile_spec.rb'
+- './ee/spec/features/projects/active_tabs_spec.rb'
+- './ee/spec/features/projects/audit_events_spec.rb'
+- './ee/spec/features/projects/custom_projects_template_spec.rb'
+- './ee/spec/features/projects/environments/environments_spec.rb'
+- './ee/spec/features/projects/feature_flags/feature_flag_issues_spec.rb'
+- './ee/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb'
+- './ee/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb'
+- './ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb'
+- './ee/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb'
+- './ee/spec/features/projects/insights_spec.rb'
+- './ee/spec/features/projects/integrations/jira_issues_list_spec.rb'
+- './ee/spec/features/projects/integrations/project_integrations_spec.rb'
+- './ee/spec/features/projects/integrations/prometheus_custom_metrics_spec.rb'
+- './ee/spec/features/projects/integrations/user_activates_github_spec.rb'
+- './ee/spec/features/projects/integrations/user_activates_jira_spec.rb'
+- './ee/spec/features/projects/issues/user_creates_issue_spec.rb'
+- './ee/spec/features/projects/issues/viewing_relocated_issues_spec.rb'
+- './ee/spec/features/projects/iterations/iteration_cadences_list_spec.rb'
+- './ee/spec/features/projects/iterations/iterations_list_spec.rb'
+- './ee/spec/features/projects/iterations/user_views_iteration_spec.rb'
+- './ee/spec/features/projects/jobs/blocked_deployment_job_page_spec.rb'
+- './ee/spec/features/projects/jobs_spec.rb'
+- './ee/spec/features/projects/kerberos_clone_instructions_spec.rb'
+- './ee/spec/features/projects/licenses/maintainer_views_policies_spec.rb'
+- './ee/spec/features/projects/members/invite_group_and_members_spec.rb'
+- './ee/spec/features/projects/members/manage_groups_spec.rb'
+- './ee/spec/features/projects/members/member_is_removed_from_project_spec.rb'
+- './ee/spec/features/projects/members/member_leaves_project_spec.rb'
+- './ee/spec/features/projects/merge_requests/user_approves_merge_request_spec.rb'
+- './ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb'
+- './ee/spec/features/projects/milestones/milestone_spec.rb'
+- './ee/spec/features/projects/mirror_spec.rb'
+- './ee/spec/features/projects/navbar_spec.rb'
+- './ee/spec/features/projects/new_project_from_template_spec.rb'
+- './ee/spec/features/projects/new_project_spec.rb'
+- './ee/spec/features/projects/path_locks_spec.rb'
+- './ee/spec/features/projects/pipelines/legacy_pipeline_spec.rb'
+- './ee/spec/features/projects/pipelines/pipeline_csp_spec.rb'
+- './ee/spec/features/projects/pipelines/pipeline_spec.rb'
+- './ee/spec/features/projects/pipelines/pipelines_spec.rb'
+- './ee/spec/features/projects/push_rules_spec.rb'
+- './ee/spec/features/projects/quality/test_case_create_spec.rb'
+- './ee/spec/features/projects/quality/test_case_list_spec.rb'
+- './ee/spec/features/projects/quality/test_case_show_spec.rb'
+- './ee/spec/features/projects/releases/user_views_release_spec.rb'
+- './ee/spec/features/projects/requirements_management/requirements_list_spec.rb'
+- './ee/spec/features/projects/security/dast_scanner_profiles_spec.rb'
+- './ee/spec/features/projects/security/dast_site_profiles_spec.rb'
+- './ee/spec/features/projects/security/user_creates_on_demand_scan_spec.rb'
+- './ee/spec/features/projects/security/user_edits_on_demand_scan_spec.rb'
+- './ee/spec/features/projects/security/user_views_security_configuration_spec.rb'
+- './ee/spec/features/projects/settings/auto_rollback_spec.rb'
+- './ee/spec/features/projects/settings/disable_merge_trains_setting_spec.rb'
+- './ee/spec/features/projects/settings/ee/repository_mirrors_settings_spec.rb'
+- './ee/spec/features/projects/settings/ee/service_desk_setting_spec.rb'
+- './ee/spec/features/projects/settings/issues_settings_spec.rb'
+- './ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb'
+- './ee/spec/features/projects/settings/merge_requests_settings_spec.rb'
+- './ee/spec/features/projects/settings/pipeline_subscriptions_spec.rb'
+- './ee/spec/features/projects/settings/protected_environments_spec.rb'
+- './ee/spec/features/projects/settings/push_rules_settings_spec.rb'
+- './ee/spec/features/projects/settings/slack_application_spec.rb'
+- './ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb'
+- './ee/spec/features/projects/settings/user_manages_issues_template_spec.rb'
+- './ee/spec/features/projects/settings/user_manages_members_spec.rb'
+- './ee/spec/features/projects/settings/user_manages_merge_pipelines_spec.rb'
+- './ee/spec/features/projects/settings/user_manages_merge_requests_template_spec.rb'
+- './ee/spec/features/projects/settings/user_manages_merge_trains_spec.rb'
+- './ee/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb'
+- './ee/spec/features/projects/show_project_spec.rb'
+- './ee/spec/features/projects_spec.rb'
+- './ee/spec/features/projects/user_applies_custom_file_template_spec.rb'
+- './ee/spec/features/projects/view_blob_with_code_owners_spec.rb'
+- './ee/spec/features/projects/wiki/user_views_wiki_empty_spec.rb'
+- './ee/spec/features/promotion_spec.rb'
+- './ee/spec/features/protected_branches_spec.rb'
+- './ee/spec/features/protected_tags_spec.rb'
+- './ee/spec/features/read_only_spec.rb'
+- './ee/spec/features/registrations/combined_registration_spec.rb'
+- './ee/spec/features/registrations/one_trust_spec.rb'
+- './ee/spec/features/registrations/saas_user_registration_spec.rb'
+- './ee/spec/features/registrations/trial_during_signup_flow_spec.rb'
+- './ee/spec/features/registrations/user_sees_new_onboarding_flow_spec.rb'
+- './ee/spec/features/registrations/welcome_spec.rb'
+- './ee/spec/features/search/elastic/global_search_spec.rb'
+- './ee/spec/features/search/elastic/group_search_spec.rb'
+- './ee/spec/features/search/elastic/project_search_spec.rb'
+- './ee/spec/features/search/elastic/snippet_search_spec.rb'
+- './ee/spec/features/search/user_searches_for_epics_spec.rb'
+- './ee/spec/features/security/admin_access_spec.rb'
+- './ee/spec/features/security/dashboard_access_spec.rb'
+- './ee/spec/features/security/group/internal_access_spec.rb'
+- './ee/spec/features/security/group/private_access_spec.rb'
+- './ee/spec/features/security/group/public_access_spec.rb'
+- './ee/spec/features/security/profile_access_spec.rb'
+- './ee/spec/features/security/project/discover_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/security/project/snippet/internal_access_spec.rb'
+- './ee/spec/features/security/project/snippet/private_access_spec.rb'
+- './ee/spec/features/security/project/snippet/public_access_spec.rb'
+- './ee/spec/features/signup_spec.rb'
+- './ee/spec/features/subscriptions/expiring_subscription_message_spec.rb'
+- './ee/spec/features/subscriptions/groups/edit_spec.rb'
+- './ee/spec/features/subscriptions_spec.rb'
+- './ee/spec/features/trial_registrations/company_information_spec.rb'
+- './ee/spec/features/trial_registrations/signin_spec.rb'
+- './ee/spec/features/trial_registrations/signup_spec.rb'
+- './ee/spec/features/trials/capture_lead_spec.rb'
+- './ee/spec/features/trials/select_namespace_spec.rb'
+- './ee/spec/features/trials/show_trial_banner_spec.rb'
+- './ee/spec/features/users/arkose_labs_csp_spec.rb'
+- './ee/spec/features/users/login_spec.rb'
+- './ee/spec/features/users/signup_spec.rb'
+- './ee/spec/features/user_unsubscribes_from_admin_notifications_spec.rb'
+- './ee/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb'
+- './ee/spec/finders/analytics/devops_adoption/enabled_namespaces_finder_spec.rb'
+- './ee/spec/finders/analytics/devops_adoption/snapshots_finder_spec.rb'
+- './ee/spec/finders/approval_rules/group_finder_spec.rb'
+- './ee/spec/finders/app_sec/fuzzing/coverage/corpuses_finder_spec.rb'
+- './ee/spec/finders/audit_event_finder_spec.rb'
+- './ee/spec/finders/auth/group_saml_identity_finder_spec.rb'
+- './ee/spec/finders/auth/provisioned_users_finder_spec.rb'
+- './ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb'
+- './ee/spec/finders/autocomplete/project_invited_groups_finder_spec.rb'
+- './ee/spec/finders/autocomplete/vulnerabilities_autocomplete_finder_spec.rb'
+- './ee/spec/finders/billed_users_finder_spec.rb'
+- './ee/spec/finders/boards/boards_finder_spec.rb'
+- './ee/spec/finders/boards/epic_boards_finder_spec.rb'
+- './ee/spec/finders/boards/milestones_finder_spec.rb'
+- './ee/spec/finders/boards/users_finder_spec.rb'
+- './ee/spec/finders/clusters/environments_finder_spec.rb'
+- './ee/spec/finders/compliance_management/merge_requests/compliance_violations_finder_spec.rb'
+- './ee/spec/finders/custom_project_templates_finder_spec.rb'
+- './ee/spec/finders/dast/profiles_finder_spec.rb'
+- './ee/spec/finders/dast_scanner_profiles_finder_spec.rb'
+- './ee/spec/finders/dast_site_profiles_finder_spec.rb'
+- './ee/spec/finders/dast_site_validations_finder_spec.rb'
+- './ee/spec/finders/ee/alert_management/http_integrations_finder_spec.rb'
+- './ee/spec/finders/ee/autocomplete/users_finder_spec.rb'
+- './ee/spec/finders/ee/ci/daily_build_group_report_results_finder_spec.rb'
+- './ee/spec/finders/ee/clusters/agent_authorizations_finder_spec.rb'
+- './ee/spec/finders/ee/clusters/agents_finder_spec.rb'
+- './ee/spec/finders/ee/fork_targets_finder_spec.rb'
+- './ee/spec/finders/ee/group_members_finder_spec.rb'
+- './ee/spec/finders/ee/namespaces/projects_finder_spec.rb'
+- './ee/spec/finders/ee/projects_finder_spec.rb'
+- './ee/spec/finders/ee/user_recent_events_finder_spec.rb'
+- './ee/spec/finders/epics_finder_spec.rb'
+- './ee/spec/finders/geo/ci_secure_file_registry_finder_spec.rb'
+- './ee/spec/finders/geo/container_repository_registry_finder_spec.rb'
+- './ee/spec/finders/geo/design_registry_finder_spec.rb'
+- './ee/spec/finders/geo/group_wiki_repository_registry_finder_spec.rb'
+- './ee/spec/finders/geo/lfs_object_registry_finder_spec.rb'
+- './ee/spec/finders/geo/merge_request_diff_registry_finder_spec.rb'
+- './ee/spec/finders/geo_node_finder_spec.rb'
+- './ee/spec/finders/geo/package_file_registry_finder_spec.rb'
+- './ee/spec/finders/geo/pages_deployment_registry_finder_spec.rb'
+- './ee/spec/finders/geo/pipeline_artifact_registry_finder_spec.rb'
+- './ee/spec/finders/geo/project_registry_finder_spec.rb'
+- './ee/spec/finders/geo/project_registry_status_finder_spec.rb'
+- './ee/spec/finders/geo/repository_verification_finder_spec.rb'
+- './ee/spec/finders/geo/snippet_repository_registry_finder_spec.rb'
+- './ee/spec/finders/geo/terraform_state_version_registry_finder_spec.rb'
+- './ee/spec/finders/geo/upload_registry_finder_spec.rb'
+- './ee/spec/finders/gpg_keys_finder_spec.rb'
+- './ee/spec/finders/group_projects_finder_spec.rb'
+- './ee/spec/finders/group_saml_identity_finder_spec.rb'
+- './ee/spec/finders/groups_with_templates_finder_spec.rb'
+- './ee/spec/finders/incident_management/escalation_policies_finder_spec.rb'
+- './ee/spec/finders/incident_management/escalation_rules_finder_spec.rb'
+- './ee/spec/finders/incident_management/issuable_resource_links_finder_spec.rb'
+- './ee/spec/finders/incident_management/member_oncall_rotations_finder_spec.rb'
+- './ee/spec/finders/incident_management/oncall_rotations_finder_spec.rb'
+- './ee/spec/finders/incident_management/oncall_schedules_finder_spec.rb'
+- './ee/spec/finders/incident_management/oncall_users_finder_spec.rb'
+- './ee/spec/finders/issues_finder_spec.rb'
+- './ee/spec/finders/iterations/cadences_finder_spec.rb'
+- './ee/spec/finders/iterations_finder_spec.rb'
+- './ee/spec/finders/licenses_finder_spec.rb'
+- './ee/spec/finders/license_template_finder_spec.rb'
+- './ee/spec/finders/merge_requests/by_approvers_finder_spec.rb'
+- './ee/spec/finders/merge_requests_finder_spec.rb'
+- './ee/spec/finders/merge_trains_finder_spec.rb'
+- './ee/spec/finders/notes_finder_spec.rb'
+- './ee/spec/finders/productivity_analytics_finder_spec.rb'
+- './ee/spec/finders/projects/integrations/jira/by_ids_finder_spec.rb'
+- './ee/spec/finders/projects/integrations/jira/issues_finder_spec.rb'
+- './ee/spec/finders/requirements_management/requirements_finder_spec.rb'
+- './ee/spec/finders/scim_finder_spec.rb'
+- './ee/spec/finders/security/findings_finder_spec.rb'
+- './ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
+- './ee/spec/finders/security/scan_execution_policies_finder_spec.rb'
+- './ee/spec/finders/security/training_providers/base_url_finder_spec.rb'
+- './ee/spec/finders/security/training_providers/kontra_url_finder_spec.rb'
+- './ee/spec/finders/security/training_providers/secure_code_warrior_url_finder_spec.rb'
+- './ee/spec/finders/security/training_urls_finder_spec.rb'
+- './ee/spec/finders/security/vulnerabilities_finder_spec.rb'
+- './ee/spec/finders/security/vulnerability_feedbacks_finder_spec.rb'
+- './ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
+- './ee/spec/finders/snippets_finder_spec.rb'
+- './ee/spec/finders/software_license_policies_finder_spec.rb'
+- './ee/spec/finders/status_page/incident_comments_finder_spec.rb'
+- './ee/spec/finders/status_page/incidents_finder_spec.rb'
+- './ee/spec/finders/template_finder_spec.rb'
+- './ee/spec/finders/users_finder_spec.rb'
+- './ee/spec/frontend/fixtures/analytics/charts.rb'
+- './ee/spec/frontend/fixtures/analytics/devops_reports/devops_adoption/enabled_namespaces.rb'
+- './ee/spec/frontend/fixtures/analytics/metrics.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams_code_stage.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams_issue_stage.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams_plan_stage.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams_review_stage.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams_staging_stage.rb'
+- './ee/spec/frontend/fixtures/analytics/value_streams_test_stage.rb'
+- './ee/spec/frontend/fixtures/codequality_report.rb'
+- './ee/spec/frontend/fixtures/dast_profiles.rb'
+- './ee/spec/frontend/fixtures/dora/metrics.rb'
+- './ee/spec/frontend/fixtures/epic.rb'
+- './ee/spec/frontend/fixtures/issues.rb'
+- './ee/spec/frontend/fixtures/merge_requests.rb'
+- './ee/spec/frontend/fixtures/on_demand_dast_scans.rb'
+- './ee/spec/frontend/fixtures/project_quality_summary.rb'
+- './ee/spec/frontend/fixtures/projects.rb'
+- './ee/spec/frontend/fixtures/runner.rb'
+- './ee/spec/frontend/fixtures/saml_providers.rb'
+- './ee/spec/frontend/fixtures/search.rb'
+- './ee/spec/graphql/api/vulnerabilities_spec.rb'
+- './ee/spec/graphql/ee/mutations/boards/issues/issue_move_list_spec.rb'
+- './ee/spec/graphql/ee/mutations/boards/lists/create_spec.rb'
+- './ee/spec/graphql/ee/mutations/ci/project_ci_cd_settings_update_spec.rb'
+- './ee/spec/graphql/ee/mutations/ci/runner/update_spec.rb'
+- './ee/spec/graphql/ee/mutations/concerns/mutations/resolves_issuable_spec.rb'
+- './ee/spec/graphql/ee/resolvers/board_list_issues_resolver_spec.rb'
+- './ee/spec/graphql/ee/resolvers/board_lists_resolver_spec.rb'
+- './ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb'
+- './ee/spec/graphql/ee/resolvers/namespace_projects_resolver_spec.rb'
+- './ee/spec/graphql/ee/types/alert_management/http_integration_type_spec.rb'
+- './ee/spec/graphql/ee/types/board_list_type_spec.rb'
+- './ee/spec/graphql/ee/types/boards/board_issue_input_type_spec.rb'
+- './ee/spec/graphql/ee/types/board_type_spec.rb'
+- './ee/spec/graphql/ee/types/ci/pipeline_merge_request_type_enum_spec.rb'
+- './ee/spec/graphql/ee/types/compliance_management/compliance_framework_type_spec.rb'
+- './ee/spec/graphql/ee/types/group_type_spec.rb'
+- './ee/spec/graphql/ee/types/issuable_type_spec.rb'
+- './ee/spec/graphql/ee/types/issue_sort_enum_spec.rb'
+- './ee/spec/graphql/ee/types/merge_request_type_spec.rb'
+- './ee/spec/graphql/ee/types/milestone_type_spec.rb'
+- './ee/spec/graphql/ee/types/mutation_type_spec.rb'
+- './ee/spec/graphql/ee/types/namespace_type_spec.rb'
+- './ee/spec/graphql/ee/types/notes/noteable_interface_spec.rb'
+- './ee/spec/graphql/ee/types/projects/service_type_enum_spec.rb'
+- './ee/spec/graphql/ee/types/repository/blob_type_spec.rb'
+- './ee/spec/graphql/ee/types/todoable_interface_spec.rb'
+- './ee/spec/graphql/ee/types/user_merge_request_interaction_type_spec.rb'
+- './ee/spec/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
+- './ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb'
+- './ee/spec/graphql/mutations/audit_events/streaming/headers/create_spec.rb'
+- './ee/spec/graphql/mutations/audit_events/streaming/headers/destroy_spec.rb'
+- './ee/spec/graphql/mutations/boards/epic_boards/create_spec.rb'
+- './ee/spec/graphql/mutations/boards/epic_boards/destroy_spec.rb'
+- './ee/spec/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+- './ee/spec/graphql/mutations/boards/epic_boards/update_spec.rb'
+- './ee/spec/graphql/mutations/boards/epic_lists/create_spec.rb'
+- './ee/spec/graphql/mutations/boards/epic_lists/update_spec.rb'
+- './ee/spec/graphql/mutations/boards/epics/create_spec.rb'
+- './ee/spec/graphql/mutations/boards/lists/update_limit_metrics_spec.rb'
+- './ee/spec/graphql/mutations/boards/update_epic_user_preferences_spec.rb'
+- './ee/spec/graphql/mutations/boards/update_spec.rb'
+- './ee/spec/graphql/mutations/compliance_management/frameworks/create_spec.rb'
+- './ee/spec/graphql/mutations/compliance_management/frameworks/destroy_spec.rb'
+- './ee/spec/graphql/mutations/compliance_management/frameworks/update_spec.rb'
+- './ee/spec/graphql/mutations/dast_on_demand_scans/create_spec.rb'
+- './ee/spec/graphql/mutations/dast/profiles/create_spec.rb'
+- './ee/spec/graphql/mutations/dast/profiles/delete_spec.rb'
+- './ee/spec/graphql/mutations/dast/profiles/run_spec.rb'
+- './ee/spec/graphql/mutations/dast/profiles/update_spec.rb'
+- './ee/spec/graphql/mutations/dast_scanner_profiles/create_spec.rb'
+- './ee/spec/graphql/mutations/dast_scanner_profiles/delete_spec.rb'
+- './ee/spec/graphql/mutations/dast_scanner_profiles/update_spec.rb'
+- './ee/spec/graphql/mutations/dast_site_profiles/create_spec.rb'
+- './ee/spec/graphql/mutations/dast_site_profiles/delete_spec.rb'
+- './ee/spec/graphql/mutations/dast_site_profiles/update_spec.rb'
+- './ee/spec/graphql/mutations/dast_site_tokens/create_spec.rb'
+- './ee/spec/graphql/mutations/dast_site_validations/create_spec.rb'
+- './ee/spec/graphql/mutations/dast_site_validations/revoke_spec.rb'
+- './ee/spec/graphql/mutations/epics/add_issue_spec.rb'
+- './ee/spec/graphql/mutations/epics/create_spec.rb'
+- './ee/spec/graphql/mutations/epics/update_spec.rb'
+- './ee/spec/graphql/mutations/gitlab_subscriptions/activate_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/escalation_policy/create_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/escalation_policy/destroy_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/escalation_policy/update_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/issuable_resource_link/create_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/issuable_resource_link/destroy_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/oncall_rotation/create_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/oncall_rotation/destroy_spec.rb'
+- './ee/spec/graphql/mutations/incident_management/oncall_rotation/update_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/graphql/mutations/instance_security_dashboard/add_project_spec.rb'
+- './ee/spec/graphql/mutations/instance_security_dashboard/remove_project_spec.rb'
+- './ee/spec/graphql/mutations/issues/create_spec.rb'
+- './ee/spec/graphql/mutations/issues/promote_to_epic_spec.rb'
+- './ee/spec/graphql/mutations/issues/set_assignees_spec.rb'
+- './ee/spec/graphql/mutations/issues/set_epic_spec.rb'
+- './ee/spec/graphql/mutations/issues/set_escalation_policy_spec.rb'
+- './ee/spec/graphql/mutations/issues/set_iteration_spec.rb'
+- './ee/spec/graphql/mutations/issues/set_weight_spec.rb'
+- './ee/spec/graphql/mutations/issues/update_spec.rb'
+- './ee/spec/graphql/mutations/merge_requests/accept_spec.rb'
+- './ee/spec/graphql/mutations/merge_requests/set_assignees_spec.rb'
+- './ee/spec/graphql/mutations/merge_requests/set_reviewers_spec.rb'
+- './ee/spec/graphql/mutations/namespaces/increase_storage_temporarily_spec.rb'
+- './ee/spec/graphql/mutations/projects/set_compliance_framework_spec.rb'
+- './ee/spec/graphql/mutations/projects/set_locked_spec.rb'
+- './ee/spec/graphql/mutations/releases/update_spec.rb'
+- './ee/spec/graphql/mutations/requirements_management/create_requirement_spec.rb'
+- './ee/spec/graphql/mutations/requirements_management/export_requirements_spec.rb'
+- './ee/spec/graphql/mutations/requirements_management/update_requirement_spec.rb'
+- './ee/spec/graphql/mutations/security/ci_configuration/configure_container_scanning_spec.rb'
+- './ee/spec/graphql/mutations/security/ci_configuration/configure_dependency_scanning_spec.rb'
+- './ee/spec/graphql/mutations/security_finding/dismiss_spec.rb'
+- './ee/spec/graphql/mutations/security_policy/assign_security_policy_project_spec.rb'
+- './ee/spec/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
+- './ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
+- './ee/spec/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
+- './ee/spec/graphql/mutations/security/training_provider_update_spec.rb'
+- './ee/spec/graphql/mutations/todos/create_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/confirm_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/create_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/dismiss_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/resolve_spec.rb'
+- './ee/spec/graphql/mutations/vulnerabilities/revert_to_detected_spec.rb'
+- './ee/spec/graphql/representation/vulnerability_scanner_entry_spec.rb'
+- './ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/admin/cloud_licenses/subscription_future_entries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/analytics/devops_adoption/enabled_namespaces_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/app_sec/dast/profile_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/app_sec/fuzzing/coverage/corpuses_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb'
+- './ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/boards/epic_boards_resolvers_spec.rb'
+- './ee/spec/graphql/resolvers/boards/epic_list_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/boards/epic_lists_resolvers_spec.rb'
+- './ee/spec/graphql/resolvers/ci/code_coverage_activities_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/ci/code_coverage_summary_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/dast_site_profile_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/dast_site_validation_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/dora_metrics_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/epic_ancestors_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/epics_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/external_issue_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/ci_secure_file_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/geo_node_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/group_wiki_repository_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/job_artifact_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/lfs_object_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/merge_request_diff_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/package_file_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/pages_deployment_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/pipeline_artifact_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/snippet_repository_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/terraform_state_version_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/geo/upload_registries_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/incident_management/escalation_policies_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/incident_management/issuable_resource_links_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/incident_management/oncall_rotations_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/incident_management/oncall_schedule_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/incident_management/oncall_shifts_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/incident_management/oncall_users_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/instance_security_dashboard/projects_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/instance_security_dashboard_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/iterations/cadences_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/iterations_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/network_policy_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/path_locks_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/pipeline_security_report_findings_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/requirements_management/test_reports_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/security_orchestration/scan_execution_policy_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/security_orchestration/scan_result_policy_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/timebox_report_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities/container_images_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities_count_per_day_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities/details_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities_grade_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities/issue_links_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerabilities/scanners_resolver_spec.rb'
+- './ee/spec/graphql/resolvers/vulnerability_severities_count_resolver_spec.rb'
+- './ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb'
+- './ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb'
+- './ee/spec/graphql/types/admin/cloud_licenses/subscription_future_entry_type_spec.rb'
+- './ee/spec/graphql/types/alert_management/payload_alert_field_name_enum_spec.rb'
+- './ee/spec/graphql/types/alert_management/payload_alert_field_path_segment_type_spec.rb'
+- './ee/spec/graphql/types/alert_management/payload_alert_field_type_enum_spec.rb'
+- './ee/spec/graphql/types/approval_rule_type_enum_spec.rb'
+- './ee/spec/graphql/types/approval_rule_type_spec.rb'
+- './ee/spec/graphql/types/app_sec/fuzzing/api/ci_configuration_type_spec.rb'
+- './ee/spec/graphql/types/app_sec/fuzzing/api/scan_mode_enum_spec.rb'
+- './ee/spec/graphql/types/app_sec/fuzzing/api/scan_profile_type_spec.rb'
+- './ee/spec/graphql/types/app_sec/fuzzing/coverage/corpus_type_spec.rb'
+- './ee/spec/graphql/types/asset_type_spec.rb'
+- './ee/spec/graphql/types/audit_events/exterrnal_audit_event_destination_type_spec.rb'
+- './ee/spec/graphql/types/audit_events/streaming/header_type_spec.rb'
+- './ee/spec/graphql/types/boards/board_epic_type_spec.rb'
+- './ee/spec/graphql/types/boards/epic_board_type_spec.rb'
+- './ee/spec/graphql/types/boards/epic_list_metadata_type_spec.rb'
+- './ee/spec/graphql/types/boards/epic_list_type_spec.rb'
+- './ee/spec/graphql/types/boards/epic_user_preferences_type_spec.rb'
+- './ee/spec/graphql/types/burnup_chart_daily_totals_type_spec.rb'
+- './ee/spec/graphql/types/ci/code_coverage_activity_type_spec.rb'
+- './ee/spec/graphql/types/ci/code_coverage_summary_spec.rb'
+- './ee/spec/graphql/types/ci/code_quality_degradation_severity_enum_spec.rb'
+- './ee/spec/graphql/types/ci/code_quality_degradation_type_spec.rb'
+- './ee/spec/graphql/types/ci/minutes/namespace_monthly_usage_type_spec.rb'
+- './ee/spec/graphql/types/ci/minutes/project_monthly_usage_type_spec.rb'
+- './ee/spec/graphql/types/ci/pipeline_type_spec.rb'
+- './ee/spec/graphql/types/ci/runner_type_spec.rb'
+- './ee/spec/graphql/types/compliance_management/merge_requests/compliance_violation_input_type_spec.rb'
+- './ee/spec/graphql/types/compliance_management/merge_requests/compliance_violation_reason_enum_spec.rb'
+- './ee/spec/graphql/types/compliance_management/merge_requests/compliance_violation_severity_enum_spec.rb'
+- './ee/spec/graphql/types/compliance_management/merge_requests/compliance_violation_sort_enum_spec.rb'
+- './ee/spec/graphql/types/compliance_management/merge_requests/compliance_violation_type_spec.rb'
+- './ee/spec/graphql/types/dast/profile_branch_type_spec.rb'
+- './ee/spec/graphql/types/dast/profile_cadence_enum_spec.rb'
+- './ee/spec/graphql/types/dast/profile_cadence_input_type_spec.rb'
+- './ee/spec/graphql/types/dast/profile_cadence_type_spec.rb'
+- './ee/spec/graphql/types/dast/profile_schedule_input_type_spec.rb'
+- './ee/spec/graphql/types/dast/profile_schedule_type_spec.rb'
+- './ee/spec/graphql/types/dast/profile_type_spec.rb'
+- './ee/spec/graphql/types/dast/scan_method_type_enum_spec.rb'
+- './ee/spec/graphql/types/dast_scanner_profile_type_spec.rb'
+- './ee/spec/graphql/types/dast/site_profile_auth_input_type_spec.rb'
+- './ee/spec/graphql/types/dast/site_profile_auth_type_spec.rb'
+- './ee/spec/graphql/types/dast_site_profile_type_spec.rb'
+- './ee/spec/graphql/types/dast_site_validation_type_spec.rb'
+- './ee/spec/graphql/types/dora_metric_bucketing_interval_enum_spec.rb'
+- './ee/spec/graphql/types/dora_metric_type_enum_spec.rb'
+- './ee/spec/graphql/types/dora_metric_type_spec.rb'
+- './ee/spec/graphql/types/dora_type_spec.rb'
+- './ee/spec/graphql/types/epic_descendant_count_type_spec.rb'
+- './ee/spec/graphql/types/epic_descendant_weight_sum_type_spec.rb'
+- './ee/spec/graphql/types/epic_issue_type_spec.rb'
+- './ee/spec/graphql/types/epic_sort_enum_spec.rb'
+- './ee/spec/graphql/types/epic_state_enum_spec.rb'
+- './ee/spec/graphql/types/epic_type_spec.rb'
+- './ee/spec/graphql/types/external_issue_type_spec.rb'
+- './ee/spec/graphql/types/geo/ci_secure_file_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/geo_node_type_spec.rb'
+- './ee/spec/graphql/types/geo/job_artifact_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/lfs_object_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/merge_request_diff_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/package_file_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/pages_deployment_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/pipeline_artifact_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/registry_state_enum_spec.rb'
+- './ee/spec/graphql/types/geo/terraform_state_version_registry_type_spec.rb'
+- './ee/spec/graphql/types/geo/upload_registry_type_spec.rb'
+- './ee/spec/graphql/types/global_id_type_spec.rb'
+- './ee/spec/graphql/types/group_release_stats_type_spec.rb'
+- './ee/spec/graphql/types/group_stats_type_spec.rb'
+- './ee/spec/graphql/types/health_status_enum_spec.rb'
+- './ee/spec/graphql/types/incident_management/escalation_policy_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/escalation_rule_input_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/escalation_rule_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/issuable_resource_link_type_enum_spec.rb'
+- './ee/spec/graphql/types/incident_management/issuable_resource_link_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/oncall_participant_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/oncall_rotation_date_input_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/oncall_rotation_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/oncall_schedule_type_spec.rb'
+- './ee/spec/graphql/types/incident_management/oncall_shift_type_spec.rb'
+- './ee/spec/graphql/types/instance_security_dashboard_type_spec.rb'
+- './ee/spec/graphql/types/issue_connection_type_spec.rb'
+- './ee/spec/graphql/types/issue_type_spec.rb'
+- './ee/spec/graphql/types/iterations/cadence_type_spec.rb'
+- './ee/spec/graphql/types/iteration_type_spec.rb'
+- './ee/spec/graphql/types/json_string_type_spec.rb'
+- './ee/spec/graphql/types/merge_requests/approval_state_type_spec.rb'
+- './ee/spec/graphql/types/metric_image_type_spec.rb'
+- './ee/spec/graphql/types/move_type_enum_spec.rb'
+- './ee/spec/graphql/types/network_policy_kind_enum_spec.rb'
+- './ee/spec/graphql/types/network_policy_type_spec.rb'
+- './ee/spec/graphql/types/path_lock_type_spec.rb'
+- './ee/spec/graphql/types/permission_types/epic_spec.rb'
+- './ee/spec/graphql/types/permission_types/project_spec.rb'
+- './ee/spec/graphql/types/permission_types/vulnerability_spec.rb'
+- './ee/spec/graphql/types/pipeline_security_report_finding_type_spec.rb'
+- './ee/spec/graphql/types/projects/services_enum_spec.rb'
+- './ee/spec/graphql/types/project_type_spec.rb'
+- './ee/spec/graphql/types/push_rules_type_spec.rb'
+- './ee/spec/graphql/types/query_type_spec.rb'
+- './ee/spec/graphql/types/requirements_management/requirement_state_enum_spec.rb'
+- './ee/spec/graphql/types/requirements_management/requirement_states_count_type_spec.rb'
+- './ee/spec/graphql/types/requirements_management/requirement_type_spec.rb'
+- './ee/spec/graphql/types/requirements_management/test_report_state_enum_spec.rb'
+- './ee/spec/graphql/types/requirements_management/test_report_type_spec.rb'
+- './ee/spec/graphql/types/scanned_resource_type_spec.rb'
+- './ee/spec/graphql/types/scan_type_spec.rb'
+- './ee/spec/graphql/types/security_orchestration/group_security_policy_source_type_spec.rb'
+- './ee/spec/graphql/types/security_orchestration/project_security_policy_source_type_spec.rb'
+- './ee/spec/graphql/types/security_orchestration/security_policy_relation_type_enum_spec.rb'
+- './ee/spec/graphql/types/security_orchestration/security_policy_source_type_spec.rb'
+- './ee/spec/graphql/types/security_report_summary_section_type_spec.rb'
+- './ee/spec/graphql/types/security_report_summary_type_spec.rb'
+- './ee/spec/graphql/types/security_scanners_spec.rb'
+- './ee/spec/graphql/types/security_scanner_type_enum_spec.rb'
+- './ee/spec/graphql/types/security/training_type_spec.rb'
+- './ee/spec/graphql/types/security/training_url_request_status_enum_spec.rb'
+- './ee/spec/graphql/types/security/training_url_type_spec.rb'
+- './ee/spec/graphql/types/timebox_report_type_spec.rb'
+- './ee/spec/graphql/types/vulnerabilities/container_image_type_spec.rb'
+- './ee/spec/graphql/types/vulnerabilities_count_by_day_type_spec.rb'
+- './ee/spec/graphql/types/vulnerabilities/link_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_confidence_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/base_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/boolean_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/code_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/commit_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/diff_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/file_location_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/int_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/list_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/markdown_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/module_location_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/table_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/text_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_details/url_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_detail_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_evidence_source_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_evidence_supporting_message_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_evidence_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability/external_issue_link_external_tracker_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability/external_issue_link_type_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability/external_issue_link_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_grade_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability_identifier_input_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_identifier_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability/issue_link_type_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability/issue_link_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/cluster_image_scanning_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/container_scanning_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/coverage_fuzzing_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/dast_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/dependency_scanning_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/generic_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/sast_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location/secret_detection_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_location_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_report_type_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability_request_response_header_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_request_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_response_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_scanner_input_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_scanner_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_scanner_vendor_input_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_severities_count_type_spec.rb'
+- './ee/spec/graphql/types/vulnerability_severity_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability_sort_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability_state_enum_spec.rb'
+- './ee/spec/graphql/types/vulnerability_type_spec.rb'
+- './ee/spec/graphql/types/vulnerable_dependency_type_spec.rb'
+- './ee/spec/graphql/types/vulnerable_kubernetes_resource_type_spec.rb'
+- './ee/spec/graphql/types/vulnerable_package_type_spec.rb'
+- './ee/spec/graphql/types/vulnerable_projects_by_grade_type_spec.rb'
+- './ee/spec/graphql/types/work_items/type_spec.rb'
+- './ee/spec/graphql/types/work_items/widget_interface_spec.rb'
+- './ee/spec/graphql/types/work_items/widgets/verification_status_type_spec.rb'
+- './ee/spec/helpers/admin/emails_helper_spec.rb'
+- './ee/spec/helpers/admin/ip_restriction_helper_spec.rb'
+- './ee/spec/helpers/admin/repo_size_limit_helper_spec.rb'
+- './ee/spec/helpers/analytics/code_review_helper_spec.rb'
+- './ee/spec/helpers/application_helper_spec.rb'
+- './ee/spec/helpers/audit_events_helper_spec.rb'
+- './ee/spec/helpers/billing_plans_helper_spec.rb'
+- './ee/spec/helpers/boards_helper_spec.rb'
+- './ee/spec/helpers/compliance_management/compliance_framework/group_settings_helper_spec.rb'
+- './ee/spec/helpers/credentials_inventory_helper_spec.rb'
+- './ee/spec/helpers/ee/access_tokens_helper_spec.rb'
+- './ee/spec/helpers/ee/admin/identities_helper_spec.rb'
+- './ee/spec/helpers/ee/application_settings_helper_spec.rb'
+- './ee/spec/helpers/ee/auth_helper_spec.rb'
+- './ee/spec/helpers/ee/blob_helper_spec.rb'
+- './ee/spec/helpers/ee/branches_helper_spec.rb'
+- './ee/spec/helpers/ee/ci/pipeline_editor_helper_spec.rb'
+- './ee/spec/helpers/ee/ci/pipelines_helper_spec.rb'
+- './ee/spec/helpers/ee/ci/runners_helper_spec.rb'
+- './ee/spec/helpers/ee/dashboard_helper_spec.rb'
+- './ee/spec/helpers/ee/emails_helper_spec.rb'
+- './ee/spec/helpers/ee/environments_helper_spec.rb'
+- './ee/spec/helpers/ee/events_helper_spec.rb'
+- './ee/spec/helpers/ee/export_helper_spec.rb'
+- './ee/spec/helpers/ee/feature_flags_helper_spec.rb'
+- './ee/spec/helpers/ee/geo_helper_spec.rb'
+- './ee/spec/helpers/ee/gitlab_routing_helper_spec.rb'
+- './ee/spec/helpers/ee/graph_helper_spec.rb'
+- './ee/spec/helpers/ee/groups/analytics/cycle_analytics_helper_spec.rb'
+- './ee/spec/helpers/ee/groups/group_members_helper_spec.rb'
+- './ee/spec/helpers/ee/groups_helper_spec.rb'
+- './ee/spec/helpers/ee/groups/settings_helper_spec.rb'
+- './ee/spec/helpers/ee/hooks_helper_spec.rb'
+- './ee/spec/helpers/ee/integrations_helper_spec.rb'
+- './ee/spec/helpers/ee/invite_members_helper_spec.rb'
+- './ee/spec/helpers/ee/issuables_helper_spec.rb'
+- './ee/spec/helpers/ee/issues_helper_spec.rb'
+- './ee/spec/helpers/ee/labels_helper_spec.rb'
+- './ee/spec/helpers/ee/learn_gitlab_helper_spec.rb'
+- './ee/spec/helpers/ee/lock_helper_spec.rb'
+- './ee/spec/helpers/ee/namespaces_helper_spec.rb'
+- './ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb'
+- './ee/spec/helpers/ee/operations_helper_spec.rb'
+- './ee/spec/helpers/ee/personal_access_tokens_helper_spec.rb'
+- './ee/spec/helpers/ee/profiles_helper_spec.rb'
+- './ee/spec/helpers/ee/projects/incidents_helper_spec.rb'
+- './ee/spec/helpers/ee/projects/pipeline_helper_spec.rb'
+- './ee/spec/helpers/ee/projects/security/api_fuzzing_configuration_helper_spec.rb'
+- './ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb'
+- './ee/spec/helpers/ee/projects/security/dast_configuration_helper_spec.rb'
+- './ee/spec/helpers/ee/projects/security/sast_configuration_helper_spec.rb'
+- './ee/spec/helpers/ee/registrations_helper_spec.rb'
+- './ee/spec/helpers/ee/releases_helper_spec.rb'
+- './ee/spec/helpers/ee/security_orchestration_helper_spec.rb'
+- './ee/spec/helpers/ee/sorting_helper_spec.rb'
+- './ee/spec/helpers/ee/subscribable_banner_helper_spec.rb'
+- './ee/spec/helpers/ee/system_note_helper_spec.rb'
+- './ee/spec/helpers/ee/todos_helper_spec.rb'
+- './ee/spec/helpers/ee/trial_helper_spec.rb'
+- './ee/spec/helpers/ee/trial_registration_helper_spec.rb'
+- './ee/spec/helpers/ee/users/callouts_helper_spec.rb'
+- './ee/spec/helpers/ee/version_check_helper_spec.rb'
+- './ee/spec/helpers/ee/welcome_helper_spec.rb'
+- './ee/spec/helpers/ee/wiki_helper_spec.rb'
+- './ee/spec/helpers/epics_helper_spec.rb'
+- './ee/spec/helpers/gitlab_subscriptions/upcoming_reconciliation_helper_spec.rb'
+- './ee/spec/helpers/groups/feature_discovery_moments_helper_spec.rb'
+- './ee/spec/helpers/groups/ldap_sync_helper_spec.rb'
+- './ee/spec/helpers/groups/security_features_helper_spec.rb'
+- './ee/spec/helpers/groups/sso_helper_spec.rb'
+- './ee/spec/helpers/incident_management/escalation_policy_helper_spec.rb'
+- './ee/spec/helpers/incident_management/oncall_schedule_helper_spec.rb'
+- './ee/spec/helpers/kerberos_helper_spec.rb'
+- './ee/spec/helpers/license_helper_spec.rb'
+- './ee/spec/helpers/license_monitoring_helper_spec.rb'
+- './ee/spec/helpers/manual_quarterly_co_term_banner_helper_spec.rb'
+- './ee/spec/helpers/markup_helper_spec.rb'
+- './ee/spec/helpers/merge_requests_helper_spec.rb'
+- './ee/spec/helpers/nav/new_dropdown_helper_spec.rb'
+- './ee/spec/helpers/nav/top_nav_helper_spec.rb'
+- './ee/spec/helpers/notes_helper_spec.rb'
+- './ee/spec/helpers/paid_feature_callout_helper_spec.rb'
+- './ee/spec/helpers/path_locks_helper_spec.rb'
+- './ee/spec/helpers/preferences_helper_spec.rb'
+- './ee/spec/helpers/prevent_forking_helper_spec.rb'
+- './ee/spec/helpers/projects_helper_spec.rb'
+- './ee/spec/helpers/projects/on_demand_scans_helper_spec.rb'
+- './ee/spec/helpers/projects/project_members_helper_spec.rb'
+- './ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb'
+- './ee/spec/helpers/projects/security/discover_helper_spec.rb'
+- './ee/spec/helpers/push_rules_helper_spec.rb'
+- './ee/spec/helpers/roadmaps_helper_spec.rb'
+- './ee/spec/helpers/routing/pseudonymization_helper_spec.rb'
+- './ee/spec/helpers/search_helper_spec.rb'
+- './ee/spec/helpers/seat_count_alert_helper_spec.rb'
+- './ee/spec/helpers/security_helper_spec.rb'
+- './ee/spec/helpers/subscriptions_helper_spec.rb'
+- './ee/spec/helpers/timeboxes_helper_spec.rb'
+- './ee/spec/helpers/trial_registrations/reassurances_helper_spec.rb'
+- './ee/spec/helpers/trial_status_widget_helper_spec.rb'
+- './ee/spec/helpers/users_helper_spec.rb'
+- './ee/spec/helpers/vulnerabilities_helper_spec.rb'
+- './ee/spec/initializers/1_settings_spec.rb'
+- './ee/spec/initializers/database_config_spec.rb'
+- './ee/spec/initializers/fog_google_https_private_urls_spec.rb'
+- './ee/spec/initializers/session_store_spec.rb'
+- './ee/spec/lib/analytics/devops_adoption/snapshot_calculator_spec.rb'
+- './ee/spec/lib/analytics/group_activity_calculator_spec.rb'
+- './ee/spec/lib/analytics/merge_request_metrics_calculator_spec.rb'
+- './ee/spec/lib/analytics/merge_request_metrics_refresh_spec.rb'
+- './ee/spec/lib/analytics/productivity_analytics_request_params_spec.rb'
+- './ee/spec/lib/analytics/refresh_approvals_data_spec.rb'
+- './ee/spec/lib/analytics/refresh_comments_data_spec.rb'
+- './ee/spec/lib/analytics/refresh_reassign_data_spec.rb'
+- './ee/spec/lib/api/entities/deployments/approval_spec.rb'
+- './ee/spec/lib/api/entities/deployments/approval_summary_spec.rb'
+- './ee/spec/lib/api/entities/merge_request_approval_setting_spec.rb'
+- './ee/spec/lib/api/entities/pending_member_spec.rb'
+- './ee/spec/lib/api/entities/protected_environments/approval_rule_for_summary_spec.rb'
+- './ee/spec/lib/api/entities/protected_environments/approval_rule_spec.rb'
+- './ee/spec/lib/api/entities/protected_environments/deploy_access_level_spec.rb'
+- './ee/spec/lib/arkose/settings_spec.rb'
+- './ee/spec/lib/arkose/verify_response_spec.rb'
+- './ee/spec/lib/audit/changes_spec.rb'
+- './ee/spec/lib/audit/details_spec.rb'
+- './ee/spec/lib/audit/external_status_check_changes_auditor_spec.rb'
+- './ee/spec/lib/audit/group_merge_request_approval_setting_changes_auditor_spec.rb'
+- './ee/spec/lib/audit/group_push_rules_changes_auditor_spec.rb'
+- './ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
+- './ee/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb'
+- './ee/spec/lib/banzai/filter/jira_private_image_link_filter_spec.rb'
+- './ee/spec/lib/banzai/filter/references/epic_reference_filter_spec.rb'
+- './ee/spec/lib/banzai/filter/references/iteration_reference_filter_spec.rb'
+- './ee/spec/lib/banzai/filter/references/label_reference_filter_spec.rb'
+- './ee/spec/lib/banzai/filter/references/vulnerability_reference_filters_spec.rb'
+- './ee/spec/lib/banzai/issuable_extractor_spec.rb'
+- './ee/spec/lib/banzai/reference_parser/epic_parser_spec.rb'
+- './ee/spec/lib/banzai/reference_parser/iteration_parser_spec.rb'
+- './ee/spec/lib/banzai/reference_parser/vulnerability_parser_spec.rb'
+- './ee/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
+- './ee/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb'
+- './ee/spec/lib/bulk_imports/groups/graphql/get_iterations_query_spec.rb'
+- './ee/spec/lib/bulk_imports/groups/pipelines/epics_pipeline_spec.rb'
+- './ee/spec/lib/bulk_imports/groups/pipelines/iterations_pipeline_spec.rb'
+- './ee/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb'
+- './ee/spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb'
+- './ee/spec/lib/bulk_imports/projects/pipelines/push_rule_pipeline_spec.rb'
+- './ee/spec/lib/compliance_management/merge_request_approval_settings/resolver_spec.rb'
+- './ee/spec/lib/container_registry/client_spec.rb'
+- './ee/spec/lib/ee/api/entities/analytics/code_review/merge_request_spec.rb'
+- './ee/spec/lib/ee/api/entities/analytics/group_activity_spec.rb'
+- './ee/spec/lib/ee/api/entities/billable_member_spec.rb'
+- './ee/spec/lib/ee/api/entities/ci/minutes/additional_pack_spec.rb'
+- './ee/spec/lib/ee/api/entities/deployment_extended_spec.rb'
+- './ee/spec/lib/ee/api/entities/experiment_spec.rb'
+- './ee/spec/lib/ee/api/entities/geo_node_status_spec.rb'
+- './ee/spec/lib/ee/api/entities/group_detail_spec.rb'
+- './ee/spec/lib/ee/api/entities/groups/repository_storage_move_spec.rb'
+- './ee/spec/lib/ee/api/entities/member_spec.rb'
+- './ee/spec/lib/ee/api/entities/project_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/conflict_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/emails_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/error_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/not_found_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/user_name_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/user_spec.rb'
+- './ee/spec/lib/ee/api/entities/scim/users_spec.rb'
+- './ee/spec/lib/ee/api/entities/user_with_admin_spec.rb'
+- './ee/spec/lib/ee/api/entities/vulnerability_export_spec.rb'
+- './ee/spec/lib/ee/api/entities/vulnerability_spec.rb'
+- './ee/spec/lib/ee/api/helpers/issues_helpers_spec.rb'
+- './ee/spec/lib/ee/api/helpers/members_helpers_spec.rb'
+- './ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb'
+- './ee/spec/lib/ee/api/helpers/scim_pagination_spec.rb'
+- './ee/spec/lib/ee/api/helpers_spec.rb'
+- './ee/spec/lib/ee/api/helpers/variables_helpers_spec.rb'
+- './ee/spec/lib/ee/audit/compliance_framework_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/audit/group_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/audit/project_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/audit/project_ci_cd_setting_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/audit/project_feature_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/audit/project_setting_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/audit/protected_branches_changes_auditor_spec.rb'
+- './ee/spec/lib/ee/backup/repositories_spec.rb'
+- './ee/spec/lib/ee/banzai/filter/sanitization_filter_spec.rb'
+- './ee/spec/lib/ee/bulk_imports/groups/stage_spec.rb'
+- './ee/spec/lib/ee/bulk_imports/projects/stage_spec.rb'
+- './ee/spec/lib/ee/event_filter_spec.rb'
+- './ee/spec/lib/ee/feature_spec.rb'
+- './ee/spec/lib/ee/gitlab/alert_management/payload/generic_spec.rb'
+- './ee/spec/lib/ee/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb'
+- './ee/spec/lib/ee/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb'
+- './ee/spec/lib/ee/gitlab/application_context_spec.rb'
+- './ee/spec/lib/ee/gitlab/application_rate_limiter_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/auth_finders_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/access_levels_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/config_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/group_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/sync/admin_users_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/sync/external_users_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/sync/groups_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/ldap/sync/proxy_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/request_authenticator_spec.rb'
+- './ee/spec/lib/ee/gitlab/auth/saml/identity_linker_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/create_security_setting_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/delete_invalid_epic_issues_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/drop_invalid_remediations_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/fix_incorrect_max_seats_used_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/migrate_job_artifact_registry_to_ssf_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/populate_namespace_statistics_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/purge_stale_security_scans_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/recalculate_vulnerability_finding_signatures_for_findings_spec.rb'
+- './ee/spec/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location_spec.rb'
+- './ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
+- './ee/spec/lib/ee/gitlab/checks/push_rules/branch_check_spec.rb'
+- './ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb'
+- './ee/spec/lib/ee/gitlab/checks/push_rules/file_size_check_spec.rb'
+- './ee/spec/lib/ee/gitlab/checks/push_rules/tag_check_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/config/entry/bridge_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/config/entry/need_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/config/entry/needs_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/config_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/jwt_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/matching/runner_matcher_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/parsers/security/common_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/abilities_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/after_config_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/external_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/security_orchestration_policy_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/activity_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/job_activity_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/size_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/reports/security/reports_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/status/build/manual_spec.rb'
+- './ee/spec/lib/ee/gitlab/ci/templates/templates_spec.rb'
+- './ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb'
+- './ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_spec.rb'
+- './ee/spec/lib/ee/gitlab/database/gitlab_schema_spec.rb'
+- './ee/spec/lib/ee/gitlab/database_spec.rb'
+- './ee/spec/lib/ee/gitlab/elastic/helper_spec.rb'
+- './ee/spec/lib/ee/gitlab/email/handler/service_desk_handler_spec.rb'
+- './ee/spec/lib/ee/gitlab/etag_caching/router/rails_spec.rb'
+- './ee/spec/lib/ee/gitlab/event_store_spec.rb'
+- './ee/spec/lib/ee/gitlab/git_access_design_spec.rb'
+- './ee/spec/lib/ee/gitlab/git_access_project_spec.rb'
+- './ee/spec/lib/ee/gitlab/git_access_snippet_spec.rb'
+- './ee/spec/lib/ee/gitlab/gon_helper_spec.rb'
+- './ee/spec/lib/ee/gitlab/group_search_results_spec.rb'
+- './ee/spec/lib/ee/gitlab/hook_data/group_member_builder_spec.rb'
+- './ee/spec/lib/ee/gitlab/hook_data/issue_builder_spec.rb'
+- './ee/spec/lib/ee/gitlab/hook_data/user_builder_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/after_export_strategies/custom_template_export_import_strategy_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/group/legacy_tree_restorer_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/group/legacy_tree_saver_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/group/tree_restorer_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/group/tree_saver_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/project/tree_restorer_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/project/tree_saver_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/repo_restorer_spec.rb'
+- './ee/spec/lib/ee/gitlab/import_export/wiki_repo_saver_spec.rb'
+- './ee/spec/lib/ee/gitlab/ip_restriction/enforcer_spec.rb'
+- './ee/spec/lib/ee/gitlab/issuable/clone/copy_resource_events_service_spec.rb'
+- './ee/spec/lib/ee/gitlab/issuable_metadata_spec.rb'
+- './ee/spec/lib/ee/gitlab/metrics/samplers/database_sampler_spec.rb'
+- './ee/spec/lib/ee/gitlab/middleware/read_only_spec.rb'
+- './ee/spec/lib/ee/gitlab/namespaces/storage/enforcement_spec.rb'
+- './ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb'
+- './ee/spec/lib/ee/gitlab/omniauth_initializer_spec.rb'
+- './ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb'
+- './ee/spec/lib/ee/gitlab/prometheus/metric_group_spec.rb'
+- './ee/spec/lib/ee/gitlab/rack_attack/request_spec.rb'
+- './ee/spec/lib/ee/gitlab/repo_path_spec.rb'
+- './ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/deprovision_service_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/filter_parser_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/provisioning_service_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/reprovision_service_spec.rb'
+- './ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb'
+- './ee/spec/lib/ee/gitlab/search_results_spec.rb'
+- './ee/spec/lib/ee/gitlab/security/scan_configuration_spec.rb'
+- './ee/spec/lib/ee/gitlab/snippet_search_results_spec.rb'
+- './ee/spec/lib/ee/gitlab/template/gitlab_ci_yml_template_spec.rb'
+- './ee/spec/lib/ee/gitlab/tracking_spec.rb'
+- './ee/spec/lib/ee/gitlab/url_builder_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage_data_non_sql_metrics_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage_data_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage/service_ping/payload_keys_processor_spec.rb'
+- './ee/spec/lib/ee/gitlab/usage/service_ping_report_spec.rb'
+- './ee/spec/lib/ee/gitlab/verify/lfs_objects_spec.rb'
+- './ee/spec/lib/ee/gitlab/verify/uploads_spec.rb'
+- './ee/spec/lib/ee/gitlab/web_hooks/rate_limiter_spec.rb'
+- './ee/spec/lib/ee/gitlab/web_ide/config/entry/global_spec.rb'
+- './ee/spec/lib/ee/service_ping/build_payload_spec.rb'
+- './ee/spec/lib/ee/service_ping/permit_data_categories_spec.rb'
+- './ee/spec/lib/ee/service_ping/service_ping_settings_spec.rb'
+- './ee/spec/lib/ee/sidebars/groups/menus/issues_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/menus/analytics_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/menus/ci_cd_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/menus/issues_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/menus/monitor_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/menus/repository_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb'
+- './ee/spec/lib/ee/sidebars/projects/panel_spec.rb'
+- './ee/spec/lib/elastic/latest/commit_config_spec.rb'
+- './ee/spec/lib/elastic/latest/config_spec.rb'
+- './ee/spec/lib/elastic/latest/custom_language_analyzers_spec.rb'
+- './ee/spec/lib/elastic/latest/git_class_proxy_spec.rb'
+- './ee/spec/lib/elastic/latest/git_instance_proxy_spec.rb'
+- './ee/spec/lib/elastic/latest/issue_config_spec.rb'
+- './ee/spec/lib/elastic/latest/merge_request_config_spec.rb'
+- './ee/spec/lib/elastic/latest/note_config_spec.rb'
+- './ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb'
+- './ee/spec/lib/elastic/latest/project_wiki_class_proxy_spec.rb'
+- './ee/spec/lib/elastic/latest/project_wiki_instance_proxy_spec.rb'
+- './ee/spec/lib/elastic/latest/routing_spec.rb'
+- './ee/spec/lib/elastic/latest/snippet_instance_proxy_spec.rb'
+- './ee/spec/lib/elastic/migration_spec.rb'
+- './ee/spec/lib/elastic/multi_version_class_proxy_spec.rb'
+- './ee/spec/lib/elastic/multi_version_instance_proxy_spec.rb'
+- './ee/spec/lib/gem_extensions/elasticsearch/model/adapter/active_record/records_spec.rb'
+- './ee/spec/lib/gem_extensions/elasticsearch/model/indexing/instance_methods_spec.rb'
+- './ee/spec/lib/gitlab/alert_management/alert_payload_field_extractor_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/aggregated/data_for_duration_chart_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/data_collector_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/data_for_duration_chart_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_closed_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_added_to_board_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_associated_with_milestone_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_label_added_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_label_removed_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_last_edited_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_closed_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_commit_at_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_label_added_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_label_removed_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_edited_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/base_dora_summary_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/change_failure_rate_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/group/stage_summary_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/lead_time_for_changes_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/lead_time_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb'
+- './ee/spec/lib/gitlab/analytics/cycle_analytics/summary/time_to_restore_service_spec.rb'
+- './ee/spec/lib/gitlab/analytics/type_of_work/tasks_by_type_spec.rb'
+- './ee/spec/lib/gitlab/audit/auditor_spec.rb'
+- './ee/spec/lib/gitlab/audit/events/preloader_spec.rb'
+- './ee/spec/lib/gitlab/audit/levels/group_spec.rb'
+- './ee/spec/lib/gitlab/audit/levels/instance_spec.rb'
+- './ee/spec/lib/gitlab/audit/levels/project_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/auth_hash_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/dynamic_settings_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/failure_handler_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/gma_membership_enforcer_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/group_lookup_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/identity_linker_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/membership_enforcer_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/membership_updater_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/response_check_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/response_store_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/session_enforcer_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/sso_enforcer_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/sso_state_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/token_actor_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/user_spec.rb'
+- './ee/spec/lib/gitlab/auth/group_saml/xml_response_spec.rb'
+- './ee/spec/lib/gitlab/auth/ldap/access_spec.rb'
+- './ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb'
+- './ee/spec/lib/gitlab/auth/ldap/person_spec.rb'
+- './ee/spec/lib/gitlab/auth/ldap/user_spec.rb'
+- './ee/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb'
+- './ee/spec/lib/gitlab/auth/o_auth/user_spec.rb'
+- './ee/spec/lib/gitlab/authority_analyzer_spec.rb'
+- './ee/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb'
+- './ee/spec/lib/gitlab/auth/saml/config_spec.rb'
+- './ee/spec/lib/gitlab/auth/saml/membership_updater_spec.rb'
+- './ee/spec/lib/gitlab/auth/saml/user_spec.rb'
+- './ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb'
+- './ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb'
+- './ee/spec/lib/gitlab/auth/smartcard/san_extension_spec.rb'
+- './ee/spec/lib/gitlab/auth/smartcard/session_enforcer_spec.rb'
+- './ee/spec/lib/gitlab/auth/smartcard/session_spec.rb'
+- './ee/spec/lib/gitlab/background_migration/migrate_requirements_to_work_items_spec.rb'
+- './ee/spec/lib/gitlab/background_migration/populate_test_reports_issue_id_spec.rb'
+- './ee/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb'
+- './ee/spec/lib/gitlab/bullet/exclusions_spec.rb'
+- './ee/spec/lib/gitlab/cache_spec.rb'
+- './ee/spec/lib/gitlab/checks/changes_access_spec.rb'
+- './ee/spec/lib/gitlab/checks/diff_check_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/entry/dast_configuration_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/entry/job_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/entry/secret_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/entry/vault/engine_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/entry/vault/secret_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/required/processor_spec.rb'
+- './ee/spec/lib/gitlab/ci/config/security_orchestration_policies/processor_spec.rb'
+- './ee/spec/lib/gitlab/cidr_spec.rb'
+- './ee/spec/lib/gitlab/ci/minutes/build_consumption_spec.rb'
+- './ee/spec/lib/gitlab/ci/minutes/cached_quota_spec.rb'
+- './ee/spec/lib/gitlab/ci/minutes/cost_factor_spec.rb'
+- './ee/spec/lib/gitlab/ci/minutes/gitlab_contribution_cost_factor_spec.rb'
+- './ee/spec/lib/gitlab/ci/minutes/runners_availability_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/license_compliance/license_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/metrics/generic_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/cluster_image_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/container_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/coverage_fuzzing_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/dependency_list_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/formatters/dast_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/formatters/dependency_list_spec.rb'
+- './ee/spec/lib/gitlab/ci/parsers/security/validators/default_branch_image_validator_spec.rb'
+- './ee/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb'
+- './ee/spec/lib/gitlab/ci/pipeline/chain/create_cross_database_associations_spec.rb'
+- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/activity_spec.rb'
+- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/job_activity_spec.rb'
+- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/size_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/coverage_fuzzing/report_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/dependency_list/dependency_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/dependency_list/report_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/license_scanning/dependency_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/license_scanning/license_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/license_scanning/reports_comparer_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/license_scanning/report_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/metrics/reports_comparer_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/metrics/report_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/security/finding_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/security/locations/cluster_image_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/security/locations/container_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/security/locations/dast_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/security/locations/dependency_scanning_spec.rb'
+- './ee/spec/lib/gitlab/ci/reports/security/remediation_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/api_security_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/api_security_latest_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_api_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/dast_api_latest_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/dast_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/dast_latest_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/dast_on_demand_api_scan_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/dast_runner_validation_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/dependency_scanning_gitlab_ci_yaml_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/license_scanning_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/sast_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/sast_iac_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/sast_latest_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/secret_detection_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/secret_detection_latest_gitlab_ci_yaml_spec.rb'
+- './ee/spec/lib/gitlab/ci/templates/secure_binaries_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/yaml_processor_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/entry_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/file_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/groups_loader_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/loader_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/reference_extractor_spec.rb'
+- './ee/spec/lib/gitlab/code_owners_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/users_loader_spec.rb'
+- './ee/spec/lib/gitlab/code_owners/validator_spec.rb'
+- './ee/spec/lib/gitlab/compliance_management/violations/approved_by_committer_spec.rb'
+- './ee/spec/lib/gitlab/compliance_management/violations/approved_by_insufficient_users_spec.rb'
+- './ee/spec/lib/gitlab/compliance_management/violations/approved_by_merge_request_author_spec.rb'
+- './ee/spec/lib/gitlab/com_spec.rb'
+- './ee/spec/lib/gitlab/console_spec.rb'
+- './ee/spec/lib/gitlab/contribution_analytics/data_collector_spec.rb'
+- './ee/spec/lib/gitlab/customers_dot/jwt_spec.rb'
+- './ee/spec/lib/gitlab/custom_file_templates_spec.rb'
+- './ee/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
+- './ee/spec/lib/gitlab/data_builder/vulnerability_spec.rb'
+- './ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb'
+- './ee/spec/lib/gitlab/elastic/client_spec.rb'
+- './ee/spec/lib/gitlab/elastic/document_reference_spec.rb'
+- './ee/spec/lib/gitlab/elastic/elasticsearch_enabled_cache_spec.rb'
+- './ee/spec/lib/gitlab/elastic/group_search_results_spec.rb'
+- './ee/spec/lib/gitlab/elastic/indexer_spec.rb'
+- './ee/spec/lib/gitlab/elastic/project_search_results_spec.rb'
+- './ee/spec/lib/gitlab/elastic/search_results_spec.rb'
+- './ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb'
+- './ee/spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
+- './ee/spec/lib/gitlab/email/message/account_validation_spec.rb'
+- './ee/spec/lib/gitlab/exclusive_lease_spec.rb'
+- './ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
+- './ee/spec/lib/gitlab/favicon_spec.rb'
+- './ee/spec/lib/gitlab/geo/base_request_spec.rb'
+- './ee/spec/lib/gitlab/geo/cron_manager_spec.rb'
+- './ee/spec/lib/gitlab/geo/event_gap_tracking_spec.rb'
+- './ee/spec/lib/gitlab/geo/geo_node_status_check_spec.rb'
+- './ee/spec/lib/gitlab/geo/geo_tasks_spec.rb'
+- './ee/spec/lib/gitlab/geo/git_push_http_spec.rb'
+- './ee/spec/lib/gitlab/geo/git_ssh_proxy_spec.rb'
+- './ee/spec/lib/gitlab/geo/health_check_spec.rb'
+- './ee/spec/lib/gitlab/geo/json_request_spec.rb'
+- './ee/spec/lib/gitlab/geo/jwt_request_decoder_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/daemon_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/event_logs_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/container_repository_updated_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/design_repository_updated_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_migrated_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/repositories_changed_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_deleted_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/lease_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
+- './ee/spec/lib/gitlab/geo/logger_spec.rb'
+- './ee/spec/lib/gitlab/geo/log_helpers_spec.rb'
+- './ee/spec/lib/gitlab/geo/oauth/login_state_spec.rb'
+- './ee/spec/lib/gitlab/geo/oauth/logout_state_spec.rb'
+- './ee/spec/lib/gitlab/geo/oauth/logout_token_spec.rb'
+- './ee/spec/lib/gitlab/geo/oauth/session_spec.rb'
+- './ee/spec/lib/gitlab/geo/registry_batcher_spec.rb'
+- './ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb'
+- './ee/spec/lib/gitlab/geo/replication/blob_retriever_spec.rb'
+- './ee/spec/lib/gitlab/geo/replicator_spec.rb'
+- './ee/spec/lib/gitlab/geo/signed_data_spec.rb'
+- './ee/spec/lib/gitlab/geo_spec.rb'
+- './ee/spec/lib/gitlab/git_access_spec.rb'
+- './ee/spec/lib/gitlab/git_access_wiki_spec.rb'
+- './ee/spec/lib/gitlab/gl_repository/identifier_spec.rb'
+- './ee/spec/lib/gitlab/gl_repository/repo_type_spec.rb'
+- './ee/spec/lib/gitlab/gl_repository_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/epics/epic_node_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/epics/lazy_epic_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/epics/lazy_links_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/issuables/lazy_links_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/issues/lazy_links_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/security_orchestration_policies/lazy_dast_profile_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/vulnerabilities/lazy_user_notes_count_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate_spec.rb'
+- './ee/spec/lib/gitlab/graphql/loaders/bulk_epic_aggregate_loader_spec.rb'
+- './ee/spec/lib/gitlab/graphql/loaders/oncall_participant_loader_spec.rb'
+- './ee/spec/lib/gitlab/group_plans_preloader_spec.rb'
+- './ee/spec/lib/gitlab/import_export/attributes_permitter_spec.rb'
+- './ee/spec/lib/gitlab/import_export/group/group_and_descendants_repo_restorer_spec.rb'
+- './ee/spec/lib/gitlab/import_export/group/group_and_descendants_repo_saver_spec.rb'
+- './ee/spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
+- './ee/spec/lib/gitlab/import_export/project/object_builder_spec.rb'
+- './ee/spec/lib/gitlab/import_sources_spec.rb'
+- './ee/spec/lib/gitlab/incident_management_spec.rb'
+- './ee/spec/lib/gitlab/ingestion/bulk_insertable_task_spec.rb'
+- './ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb'
+- './ee/spec/lib/gitlab/insights/executors/issuable_executor_spec.rb'
+- './ee/spec/lib/gitlab/insights/finders/issuable_finder_spec.rb'
+- './ee/spec/lib/gitlab/insights/finders/projects_finder_spec.rb'
+- './ee/spec/lib/gitlab/insights/loader_spec.rb'
+- './ee/spec/lib/gitlab/insights/project_insights_config_spec.rb'
+- './ee/spec/lib/gitlab/insights/reducers/base_reducer_spec.rb'
+- './ee/spec/lib/gitlab/insights/reducers/count_per_label_reducer_spec.rb'
+- './ee/spec/lib/gitlab/insights/reducers/count_per_period_reducer_spec.rb'
+- './ee/spec/lib/gitlab/insights/reducers/dora_reducer_spec.rb'
+- './ee/spec/lib/gitlab/insights/reducers/label_count_per_period_reducer_spec.rb'
+- './ee/spec/lib/gitlab/insights/serializers/chartjs/bar_serializer_spec.rb'
+- './ee/spec/lib/gitlab/insights/serializers/chartjs/bar_time_series_serializer_spec.rb'
+- './ee/spec/lib/gitlab/insights/serializers/chartjs/line_serializer_spec.rb'
+- './ee/spec/lib/gitlab/insights/serializers/chartjs/multi_series_serializer_spec.rb'
+- './ee/spec/lib/gitlab/insights/validators/params_validator_spec.rb'
+- './ee/spec/lib/gitlab/instrumentation/elasticsearch_transport_spec.rb'
+- './ee/spec/lib/gitlab/instrumentation_helper_spec.rb'
+- './ee/spec/lib/gitlab/ip_address_state_spec.rb'
+- './ee/spec/lib/gitlab/items_collection_spec.rb'
+- './ee/spec/lib/gitlab/kerberos/authentication_spec.rb'
+- './ee/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
+- './ee/spec/lib/gitlab/licenses/submit_license_usage_data_banner_spec.rb'
+- './ee/spec/lib/gitlab/manual_quarterly_co_term_banner_spec.rb'
+- './ee/spec/lib/gitlab/metrics/samplers/global_search_sampler_spec.rb'
+- './ee/spec/lib/gitlab/middleware/ip_restrictor_spec.rb'
+- './ee/spec/lib/gitlab/mirror_spec.rb'
+- './ee/spec/lib/gitlab/object_hierarchy_spec.rb'
+- './ee/spec/lib/gitlab/pagination_delegate_spec.rb'
+- './ee/spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb'
+- './ee/spec/lib/gitlab/patch/database_config_spec.rb'
+- './ee/spec/lib/gitlab/patch/draw_route_spec.rb'
+- './ee/spec/lib/gitlab/patch/geo_database_tasks_spec.rb'
+- './ee/spec/lib/gitlab/path_locks_finder_spec.rb'
+- './ee/spec/lib/gitlab/project_template_spec.rb'
+- './ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
+- './ee/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
+- './ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
+- './ee/spec/lib/gitlab/proxy_spec.rb'
+- './ee/spec/lib/gitlab/quick_actions/users_extractor_spec.rb'
+- './ee/spec/lib/gitlab/rack_attack_spec.rb'
+- './ee/spec/lib/gitlab/reference_extractor_spec.rb'
+- './ee/spec/lib/gitlab/regex_spec.rb'
+- './ee/spec/lib/gitlab/return_to_location_spec.rb'
+- './ee/spec/lib/gitlab/search/aggregation_parser_spec.rb'
+- './ee/spec/lib/gitlab/search/aggregation_spec.rb'
+- './ee/spec/lib/gitlab/search_context/builder_spec.rb'
+- './ee/spec/lib/gitlab/search/recent_epics_spec.rb'
+- './ee/spec/lib/gitlab/sidekiq_config_spec.rb'
+- './ee/spec/lib/gitlab/sitemaps/generator_spec.rb'
+- './ee/spec/lib/gitlab/sitemaps/sitemap_file_spec.rb'
+- './ee/spec/lib/gitlab/sitemaps/url_extractor_spec.rb'
+- './ee/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb'
+- './ee/spec/lib/gitlab/spdx/catalogue_gateway_spec.rb'
+- './ee/spec/lib/gitlab/spdx/catalogue_spec.rb'
+- './ee/spec/lib/gitlab/status_page/filter/image_filter_spec.rb'
+- './ee/spec/lib/gitlab/status_page/filter/mention_anonymization_filter_spec.rb'
+- './ee/spec/lib/gitlab/status_page/pipeline/post_process_pipeline_spec.rb'
+- './ee/spec/lib/gitlab/status_page_spec.rb'
+- './ee/spec/lib/gitlab/status_page/storage/s3_client_spec.rb'
+- './ee/spec/lib/gitlab/status_page/storage/s3_multipart_upload_spec.rb'
+- './ee/spec/lib/gitlab/status_page/storage_spec.rb'
+- './ee/spec/lib/gitlab/status_page/usage_data_counters/incident_counter_spec.rb'
+- './ee/spec/lib/gitlab/subscription_portal/clients/graphql_spec.rb'
+- './ee/spec/lib/gitlab/subscription_portal/client_spec.rb'
+- './ee/spec/lib/gitlab/subscription_portal/clients/rest_spec.rb'
+- './ee/spec/lib/gitlab_subscriptions/upcoming_reconciliation_entity_spec.rb'
+- './ee/spec/lib/gitlab/template/custom_templates_spec.rb'
+- './ee/spec/lib/gitlab/tracking/snowplow_schema_validation_spec.rb'
+- './ee/spec/lib/gitlab/tracking/standard_context_spec.rb'
+- './ee/spec/lib/gitlab/tree_summary_spec.rb'
+- './ee/spec/lib/gitlab/usage_data_counters/epic_activity_unique_counter_spec.rb'
+- './ee/spec/lib/gitlab/usage_data_counters/licenses_list_spec.rb'
+- './ee/spec/lib/gitlab/usage_data_metrics_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/advanced_search/build_type_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/advanced_search/distribution_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/advanced_search/lucene_version_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/advanced_search/version_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/approval_project_rules_with_user_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_builds_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_environments_approval_required_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_deployment_approvals_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_distinct_merged_merge_requests_using_approval_rules_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_event_streaming_destinations_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_external_status_checks_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_groups_with_event_streaming_destinations_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_external_status_checks_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_saml_group_links_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_gbp_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_users_associating_group_milestones_to_releases_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_ci_builds_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_users_deployment_approvals_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/historical_max_users_metrics_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/licensee_metrics_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/license_management_jobs_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/license_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/protected_environment_approval_rules_required_approvals_average_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/protected_environments_required_approvals_average_metric_spec.rb'
+- './ee/spec/lib/gitlab/usage/metrics/instrumentations/user_cap_setting_enabled_metric_spec.rb'
+- './ee/spec/lib/gitlab/user_access_spec.rb'
+- './ee/spec/lib/gitlab/visibility_level_spec.rb'
+- './ee/spec/lib/gitlab/vulnerabilities/base_vulnerability_spec.rb'
+- './ee/spec/lib/gitlab/vulnerabilities/container_scanning_vulnerability_spec.rb'
+- './ee/spec/lib/gitlab/vulnerabilities/findings_preloader_spec.rb'
+- './ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb'
+- './ee/spec/lib/gitlab/vulnerabilities/standard_vulnerability_spec.rb'
+- './ee/spec/lib/gitlab/web_ide/config/entry/schema/match_spec.rb'
+- './ee/spec/lib/gitlab/web_ide/config/entry/schema_spec.rb'
+- './ee/spec/lib/gitlab/web_ide/config/entry/schemas_spec.rb'
+- './ee/spec/lib/gitlab/web_ide/config/entry/schema/uri_spec.rb'
+- './ee/spec/lib/incident_management/oncall_shift_generator_spec.rb'
+- './ee/spec/lib/omni_auth/strategies/group_saml_spec.rb'
+- './ee/spec/lib/omni_auth/strategies/kerberos_spec.rb'
+- './ee/spec/lib/peek/views/elasticsearch_spec.rb'
+- './ee/spec/lib/sidebars/groups/menus/administration_menu_spec.rb'
+- './ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb'
+- './ee/spec/lib/sidebars/groups/menus/epics_menu_spec.rb'
+- './ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb'
+- './ee/spec/lib/sidebars/groups/menus/trial_experiment_menu_spec.rb'
+- './ee/spec/lib/sidebars/groups/menus/wiki_menu_spec.rb'
+- './ee/spec/lib/sidebars/projects/menus/trial_experiment_menu_spec.rb'
+- './ee/spec/lib/system_check/app/search_check_spec.rb'
+- './ee/spec/lib/system_check/geo/authorized_keys_check_spec.rb'
+- './ee/spec/lib/system_check/geo/authorized_keys_flag_check_spec.rb'
+- './ee/spec/lib/system_check/geo/current_node_check_spec.rb'
+- './ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb'
+- './ee/spec/lib/system_check/geo/http_clone_enabled_check_spec.rb'
+- './ee/spec/lib/system_check/geo/http_connection_check_spec.rb'
+- './ee/spec/lib/system_check/geo/license_check_spec.rb'
+- './ee/spec/lib/system_check/rake_task/geo_task_spec.rb'
+- './ee/spec/lib/world_spec.rb'
+- './ee/spec/mailers/ci_minutes_usage_mailer_spec.rb'
+- './ee/spec/mailers/credentials_inventory_mailer_spec.rb'
+- './ee/spec/mailers/devise_mailer_spec.rb'
+- './ee/spec/mailers/ee/emails/admin_notification_spec.rb'
+- './ee/spec/mailers/ee/emails/issues_spec.rb'
+- './ee/spec/mailers/ee/emails/merge_requests_spec.rb'
+- './ee/spec/mailers/ee/emails/profile_spec.rb'
+- './ee/spec/mailers/ee/emails/projects_spec.rb'
+- './ee/spec/mailers/emails/epics_spec.rb'
+- './ee/spec/mailers/emails/group_memberships_spec.rb'
+- './ee/spec/mailers/emails/in_product_marketing_spec.rb'
+- './ee/spec/mailers/emails/merge_commits_spec.rb'
+- './ee/spec/mailers/emails/namespace_storage_usage_mailer_spec.rb'
+- './ee/spec/mailers/emails/requirements_spec.rb'
+- './ee/spec/mailers/emails/user_cap_spec.rb'
+- './ee/spec/mailers/license_mailer_spec.rb'
+- './ee/spec/mailers/notify_spec.rb'
+- './ee/spec/migrations/20220411173544_cleanup_orphans_approval_project_rules_spec.rb'
+- './ee/spec/migrations/20220517144749_remove_vulnerability_approval_rules_spec.rb'
+- './ee/spec/migrations/add_non_null_constraint_for_escalation_rule_on_pending_alert_escalations_spec.rb'
+- './ee/spec/migrations/async_build_trace_expire_at_index_spec.rb'
+- './ee/spec/migrations/backfill_delayed_group_deletion_spec.rb'
+- './ee/spec/migrations/backfill_namespace_statistics_with_wiki_size_spec.rb'
+- './ee/spec/migrations/drop_invalid_remediations_spec.rb'
+- './ee/spec/migrations/geo/fix_state_column_in_file_registry_spec.rb'
+- './ee/spec/migrations/geo/fix_state_column_in_lfs_object_registry_spec.rb'
+- './ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb'
+- './ee/spec/migrations/geo/migrate_job_artifact_registry_spec.rb'
+- './ee/spec/migrations/geo/migrate_lfs_objects_to_separate_registry_spec.rb'
+- './ee/spec/migrations/geo/set_resync_flag_for_retried_projects_spec.rb'
+- './ee/spec/migrations/remove_schedule_and_status_null_constraints_from_pending_escalations_alert_spec.rb'
+- './ee/spec/migrations/schedule_delete_invalid_epic_issues_revised_spec.rb'
+- './ee/spec/migrations/schedule_populate_test_reports_issue_id_spec.rb'
+- './ee/spec/migrations/schedule_requirements_migration_spec.rb'
+- './ee/spec/migrations/schedule_trace_expiry_removal_spec.rb'
+- './ee/spec/migrations/update_gitlab_subscriptions_start_at_post_eoa_spec.rb'
+- './ee/spec/migrations/update_vulnerability_occurrences_location_spec.rb'
+- './ee/spec/models/alert_management/alert_payload_field_spec.rb'
+- './ee/spec/models/allowed_email_domain_spec.rb'
+- './ee/spec/models/analytics/cycle_analytics/aggregation_context_spec.rb'
+- './ee/spec/models/analytics/cycle_analytics/group_level_spec.rb'
+- './ee/spec/models/analytics/cycle_analytics/group_stage_spec.rb'
+- './ee/spec/models/analytics/cycle_analytics/group_value_stream_spec.rb'
+- './ee/spec/models/analytics/cycle_analytics/project_stage_spec.rb'
+- './ee/spec/models/analytics/cycle_analytics/runtime_limiter_spec.rb'
+- './ee/spec/models/analytics/devops_adoption/enabled_namespace_spec.rb'
+- './ee/spec/models/analytics/devops_adoption/snapshot_spec.rb'
+- './ee/spec/models/analytics/issues_analytics_spec.rb'
+- './ee/spec/models/analytics/language_trend/repository_language_spec.rb'
+- './ee/spec/models/application_setting_spec.rb'
+- './ee/spec/models/approvable_spec.rb'
+- './ee/spec/models/approval_merge_request_rule_spec.rb'
+- './ee/spec/models/approval_project_rule_spec.rb'
+- './ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb'
+- './ee/spec/models/approval_state_spec.rb'
+- './ee/spec/models/approvals/wrapped_rule_set_spec.rb'
+- './ee/spec/models/approval_wrapped_any_approver_rule_spec.rb'
+- './ee/spec/models/approval_wrapped_code_owner_rule_spec.rb'
+- './ee/spec/models/approval_wrapped_rule_spec.rb'
+- './ee/spec/models/approver_group_spec.rb'
+- './ee/spec/models/app_sec/fuzzing/api/ci_configuration_spec.rb'
+- './ee/spec/models/app_sec/fuzzing/coverage/corpus_spec.rb'
+- './ee/spec/models/audit_events/external_audit_event_destination_spec.rb'
+- './ee/spec/models/audit_events/streaming/header_spec.rb'
+- './ee/spec/models/board_assignee_spec.rb'
+- './ee/spec/models/board_label_spec.rb'
+- './ee/spec/models/boards/epic_board_label_spec.rb'
+- './ee/spec/models/boards/epic_board_position_spec.rb'
+- './ee/spec/models/boards/epic_board_recent_visit_spec.rb'
+- './ee/spec/models/boards/epic_board_spec.rb'
+- './ee/spec/models/boards/epic_list_spec.rb'
+- './ee/spec/models/boards/epic_list_user_preference_spec.rb'
+- './ee/spec/models/boards/epic_user_preference_spec.rb'
+- './ee/spec/models/board_spec.rb'
+- './ee/spec/models/board_user_preference_spec.rb'
+- './ee/spec/models/broadcast_message_spec.rb'
+- './ee/spec/models/burndown_spec.rb'
+- './ee/spec/models/ci/bridge_spec.rb'
+- './ee/spec/models/ci/build_spec.rb'
+- './ee/spec/models/ci/daily_build_group_report_result_spec.rb'
+- './ee/spec/models/ci/minutes/additional_pack_spec.rb'
+- './ee/spec/models/ci/minutes/context_spec.rb'
+- './ee/spec/models/ci/minutes/limit_spec.rb'
+- './ee/spec/models/ci/minutes/namespace_monthly_usage_spec.rb'
+- './ee/spec/models/ci/minutes/notification_spec.rb'
+- './ee/spec/models/ci/minutes/project_monthly_usage_spec.rb'
+- './ee/spec/models/ci/minutes/usage_spec.rb'
+- './ee/spec/models/ci/pipeline_spec.rb'
+- './ee/spec/models/ci/processable_spec.rb'
+- './ee/spec/models/ci/sources/project_spec.rb'
+- './ee/spec/models/ci/subscriptions/project_spec.rb'
+- './ee/spec/models/commit_spec.rb'
+- './ee/spec/models/compliance_management/compliance_framework/project_settings_spec.rb'
+- './ee/spec/models/compliance_management/framework_spec.rb'
+- './ee/spec/models/concerns/approval_rule_like_spec.rb'
+- './ee/spec/models/concerns/approver_migrate_hook_spec.rb'
+- './ee/spec/models/concerns/auditable_spec.rb'
+- './ee/spec/models/concerns/deprecated_approvals_before_merge_spec.rb'
+- './ee/spec/models/concerns/ee/clusters/agents/authorization_config_scopes_spec.rb'
+- './ee/spec/models/concerns/ee/issuable_spec.rb'
+- './ee/spec/models/concerns/ee/mentionable_spec.rb'
+- './ee/spec/models/concerns/ee/milestoneable_spec.rb'
+- './ee/spec/models/concerns/ee/noteable_spec.rb'
+- './ee/spec/models/concerns/ee/participable_spec.rb'
+- './ee/spec/models/concerns/ee/project_security_scanners_information_spec.rb'
+- './ee/spec/models/concerns/ee/weight_eventable_spec.rb'
+- './ee/spec/models/concerns/elastic/application_versioned_search_spec.rb'
+- './ee/spec/models/concerns/elastic/issue_spec.rb'
+- './ee/spec/models/concerns/elastic/merge_request_spec.rb'
+- './ee/spec/models/concerns/elastic/milestone_spec.rb'
+- './ee/spec/models/concerns/elastic/note_spec.rb'
+- './ee/spec/models/concerns/elastic/project_spec.rb'
+- './ee/spec/models/concerns/elastic/projects_search_spec.rb'
+- './ee/spec/models/concerns/elastic/project_wiki_spec.rb'
+- './ee/spec/models/concerns/elastic/repository_spec.rb'
+- './ee/spec/models/concerns/elastic/snippet_spec.rb'
+- './ee/spec/models/concerns/epic_tree_sorting_spec.rb'
+- './ee/spec/models/concerns/geo/eventable_spec.rb'
+- './ee/spec/models/concerns/geo/has_replicator_spec.rb'
+- './ee/spec/models/concerns/geo/replicable_model_spec.rb'
+- './ee/spec/models/concerns/geo/verifiable_model_spec.rb'
+- './ee/spec/models/concerns/geo/verification_state_spec.rb'
+- './ee/spec/models/concerns/health_status_spec.rb'
+- './ee/spec/models/concerns/incident_management/base_pending_escalation_spec.rb'
+- './ee/spec/models/concerns/password_complexity_spec.rb'
+- './ee/spec/models/concerns/scim_paginatable_spec.rb'
+- './ee/spec/models/container_registry/event_spec.rb'
+- './ee/spec/models/container_repository_spec.rb'
+- './ee/spec/models/dast/branch_spec.rb'
+- './ee/spec/models/dast/profile_schedule_spec.rb'
+- './ee/spec/models/dast/profile_spec.rb'
+- './ee/spec/models/dast/profiles_pipeline_spec.rb'
+- './ee/spec/models/dast/scanner_profiles_build_spec.rb'
+- './ee/spec/models/dast_scanner_profile_spec.rb'
+- './ee/spec/models/dast/site_profiles_build_spec.rb'
+- './ee/spec/models/dast/site_profile_secret_variable_spec.rb'
+- './ee/spec/models/dast_site_profile_spec.rb'
+- './ee/spec/models/dast_site_spec.rb'
+- './ee/spec/models/dast_site_token_spec.rb'
+- './ee/spec/models/dast_site_validation_spec.rb'
+- './ee/spec/models/deployments/approval_spec.rb'
+- './ee/spec/models/deployment_spec.rb'
+- './ee/spec/models/dora/base_metric_spec.rb'
+- './ee/spec/models/dora/change_failure_rate_metric_spec.rb'
+- './ee/spec/models/dora/daily_metrics_spec.rb'
+- './ee/spec/models/dora/deployment_frequency_metric_spec.rb'
+- './ee/spec/models/dora/lead_time_for_changes_metric_spec.rb'
+- './ee/spec/models/dora/time_to_restore_service_metric_spec.rb'
+- './ee/spec/models/ee/alert_management/alert_spec.rb'
+- './ee/spec/models/ee/analytics/cycle_analytics/stage_event_hash_spec.rb'
+- './ee/spec/models/ee/analytics/usage_trends/measurement_spec.rb'
+- './ee/spec/models/ee/appearance_spec.rb'
+- './ee/spec/models/ee/audit_event_spec.rb'
+- './ee/spec/models/ee/award_emoji_spec.rb'
+- './ee/spec/models/ee/ci/build_dependencies_spec.rb'
+- './ee/spec/models/ee/ci/job_artifact_spec.rb'
+- './ee/spec/models/ee/ci/pending_build_spec.rb'
+- './ee/spec/models/ee/ci/pipeline_artifact_spec.rb'
+- './ee/spec/models/ee/ci/runner_spec.rb'
+- './ee/spec/models/ee/ci/secure_file_spec.rb'
+- './ee/spec/models/ee/clusters/agent_spec.rb'
+- './ee/spec/models/ee/description_version_spec.rb'
+- './ee/spec/models/ee/event_collection_spec.rb'
+- './ee/spec/models/ee/event_spec.rb'
+- './ee/spec/models/ee/gpg_key_spec.rb'
+- './ee/spec/models/ee/group_group_link_spec.rb'
+- './ee/spec/models/ee/groups/feature_setting_spec.rb'
+- './ee/spec/models/ee/group_spec.rb'
+- './ee/spec/models/ee/incident_management/project_incident_management_setting_spec.rb'
+- './ee/spec/models/ee/integrations/jira_spec.rb'
+- './ee/spec/models/ee/integration_spec.rb'
+- './ee/spec/models/ee/iterations/cadence_spec.rb'
+- './ee/spec/models/ee/iteration_spec.rb'
+- './ee/spec/models/ee/key_spec.rb'
+- './ee/spec/models/ee/label_spec.rb'
+- './ee/spec/models/ee/lfs_object_spec.rb'
+- './ee/spec/models/ee/list_spec.rb'
+- './ee/spec/models/ee/members_preloader_spec.rb'
+- './ee/spec/models/ee/merge_request_diff_spec.rb'
+- './ee/spec/models/ee/merge_request/metrics_spec.rb'
+- './ee/spec/models/ee/namespace_ci_cd_setting_spec.rb'
+- './ee/spec/models/ee/namespace/root_storage_statistics_spec.rb'
+- './ee/spec/models/ee/namespaces/namespace_ban_spec.rb'
+- './ee/spec/models/ee/namespace_spec.rb'
+- './ee/spec/models/ee/namespace_statistics_spec.rb'
+- './ee/spec/models/ee/namespace/storage/notification_spec.rb'
+- './ee/spec/models/ee/notification_setting_spec.rb'
+- './ee/spec/models/ee/pages_deployment_spec.rb'
+- './ee/spec/models/ee/personal_access_token_spec.rb'
+- './ee/spec/models/ee/preloaders/group_policy_preloader_spec.rb'
+- './ee/spec/models/ee/project_authorization_spec.rb'
+- './ee/spec/models/ee/project_group_link_spec.rb'
+- './ee/spec/models/ee/project_setting_spec.rb'
+- './ee/spec/models/ee/project_wiki_spec.rb'
+- './ee/spec/models/ee/protected_branch_spec.rb'
+- './ee/spec/models/ee/protected_ref_access_spec.rb'
+- './ee/spec/models/ee/protected_ref_spec.rb'
+- './ee/spec/models/ee/release_spec.rb'
+- './ee/spec/models/ee/resource_label_event_spec.rb'
+- './ee/spec/models/ee/resource_state_event_spec.rb'
+- './ee/spec/models/ee/service_desk_setting_spec.rb'
+- './ee/spec/models/ee/system_note_metadata_spec.rb'
+- './ee/spec/models/ee/terraform/state_version_spec.rb'
+- './ee/spec/models/ee/user_highest_role_spec.rb'
+- './ee/spec/models/ee/users/merge_request_interaction_spec.rb'
+- './ee/spec/models/ee/user_spec.rb'
+- './ee/spec/models/ee/users_statistics_spec.rb'
+- './ee/spec/models/ee/vulnerability_spec.rb'
+- './ee/spec/models/ee/work_items/type_spec.rb'
+- './ee/spec/models/elastic/index_setting_spec.rb'
+- './ee/spec/models/elastic/migration_record_spec.rb'
+- './ee/spec/models/elastic/reindexing_slice_spec.rb'
+- './ee/spec/models/elastic/reindexing_subtask_spec.rb'
+- './ee/spec/models/elastic/reindexing_task_spec.rb'
+- './ee/spec/models/elasticsearch_indexed_namespace_spec.rb'
+- './ee/spec/models/elasticsearch_indexed_project_spec.rb'
+- './ee/spec/models/environment_spec.rb'
+- './ee/spec/models/epic_issue_spec.rb'
+- './ee/spec/models/epic/related_epic_link_spec.rb'
+- './ee/spec/models/epic_spec.rb'
+- './ee/spec/models/epic_user_mention_spec.rb'
+- './ee/spec/models/geo/cache_invalidation_event_spec.rb'
+- './ee/spec/models/geo/ci_secure_file_registry_spec.rb'
+- './ee/spec/models/geo/container_repository_registry_spec.rb'
+- './ee/spec/models/geo/container_repository_updated_event_spec.rb'
+- './ee/spec/models/geo/deleted_project_spec.rb'
+- './ee/spec/models/geo/design_registry_spec.rb'
+- './ee/spec/models/geo/event_log_spec.rb'
+- './ee/spec/models/geo/event_log_state_spec.rb'
+- './ee/spec/models/geo/every_geo_event_spec.rb'
+- './ee/spec/models/geo/group_wiki_repository_registry_spec.rb'
+- './ee/spec/models/geo/hashed_storage_migrated_event_spec.rb'
+- './ee/spec/models/geo/job_artifact_registry_spec.rb'
+- './ee/spec/models/geo/lfs_object_registry_spec.rb'
+- './ee/spec/models/geo/merge_request_diff_registry_spec.rb'
+- './ee/spec/models/geo_node_namespace_link_spec.rb'
+- './ee/spec/models/geo_node_spec.rb'
+- './ee/spec/models/geo_node_status_spec.rb'
+- './ee/spec/models/geo/package_file_registry_spec.rb'
+- './ee/spec/models/geo/pages_deployment_registry_spec.rb'
+- './ee/spec/models/geo/pipeline_artifact_registry_spec.rb'
+- './ee/spec/models/geo/project_registry_spec.rb'
+- './ee/spec/models/geo/push_user_spec.rb'
+- './ee/spec/models/geo/repositories_changed_event_spec.rb'
+- './ee/spec/models/geo/repository_created_event_spec.rb'
+- './ee/spec/models/geo/repository_renamed_event_spec.rb'
+- './ee/spec/models/geo/repository_updated_event_spec.rb'
+- './ee/spec/models/geo/reset_checksum_event_spec.rb'
+- './ee/spec/models/geo/secondary_usage_data_spec.rb'
+- './ee/spec/models/geo/snippet_repository_registry_spec.rb'
+- './ee/spec/models/geo/terraform_state_version_registry_spec.rb'
+- './ee/spec/models/geo/tracking_base_spec.rb'
+- './ee/spec/models/geo/upload_registry_spec.rb'
+- './ee/spec/models/geo/upload_state_spec.rb'
+- './ee/spec/models/gitlab/seat_link_data_spec.rb'
+- './ee/spec/models/gitlab_subscription_history_spec.rb'
+- './ee/spec/models/gitlab_subscriptions/features_spec.rb'
+- './ee/spec/models/gitlab_subscription_spec.rb'
+- './ee/spec/models/gitlab_subscriptions/upcoming_reconciliation_spec.rb'
+- './ee/spec/models/group_deletion_schedule_spec.rb'
+- './ee/spec/models/group_member_spec.rb'
+- './ee/spec/models/group_merge_request_approval_setting_spec.rb'
+- './ee/spec/models/groups/repository_storage_move_spec.rb'
+- './ee/spec/models/group_wiki_repository_spec.rb'
+- './ee/spec/models/group_wiki_spec.rb'
+- './ee/spec/models/historical_data_spec.rb'
+- './ee/spec/models/hooks/group_hook_spec.rb'
+- './ee/spec/models/identity_spec.rb'
+- './ee/spec/models/incident_management/escalation_policy_spec.rb'
+- './ee/spec/models/incident_management/escalation_rule_spec.rb'
+- './ee/spec/models/incident_management/issuable_escalation_status_spec.rb'
+- './ee/spec/models/incident_management/issuable_resource_link_spec.rb'
+- './ee/spec/models/incident_management/oncall_participant_spec.rb'
+- './ee/spec/models/incident_management/oncall_rotation_spec.rb'
+- './ee/spec/models/incident_management/oncall_schedule_spec.rb'
+- './ee/spec/models/incident_management/oncall_shift_spec.rb'
+- './ee/spec/models/incident_management/pending_escalations/alert_spec.rb'
+- './ee/spec/models/incident_management/pending_escalations/issue_spec.rb'
+- './ee/spec/models/instance_security_dashboard_spec.rb'
+- './ee/spec/models/integrations/chat_message/vulnerability_message_spec.rb'
+- './ee/spec/models/integrations/github/remote_project_spec.rb'
+- './ee/spec/models/integrations/github_spec.rb'
+- './ee/spec/models/integrations/github/status_message_spec.rb'
+- './ee/spec/models/integrations/github/status_notifier_spec.rb'
+- './ee/spec/models/integrations/gitlab_slack_application_spec.rb'
+- './ee/spec/models/ip_restriction_spec.rb'
+- './ee/spec/models/issuable_metric_image_spec.rb'
+- './ee/spec/models/issuables_analytics_spec.rb'
+- './ee/spec/models/issuable_sla_spec.rb'
+- './ee/spec/models/issue_link_spec.rb'
+- './ee/spec/models/issue_spec.rb'
+- './ee/spec/models/iteration_note_spec.rb'
+- './ee/spec/models/label_note_spec.rb'
+- './ee/spec/models/ldap_group_link_spec.rb'
+- './ee/spec/models/license_spec.rb'
+- './ee/spec/models/member_spec.rb'
+- './ee/spec/models/merge_request/blocking_spec.rb'
+- './ee/spec/models/merge_request_block_spec.rb'
+- './ee/spec/models/merge_requests/compliance_violation_spec.rb'
+- './ee/spec/models/merge_requests/external_status_check_spec.rb'
+- './ee/spec/models/merge_request_spec.rb'
+- './ee/spec/models/merge_requests/status_check_response_spec.rb'
+- './ee/spec/models/merge_train_spec.rb'
+- './ee/spec/models/milestone_release_spec.rb'
+- './ee/spec/models/milestone_spec.rb'
+- './ee/spec/models/namespace_limit_spec.rb'
+- './ee/spec/models/namespace_setting_spec.rb'
+- './ee/spec/models/namespaces/free_user_cap/preview_spec.rb'
+- './ee/spec/models/namespaces/free_user_cap_spec.rb'
+- './ee/spec/models/namespaces/free_user_cap/standard_spec.rb'
+- './ee/spec/models/namespaces/storage/root_excess_size_spec.rb'
+- './ee/spec/models/namespaces/storage/root_size_spec.rb'
+- './ee/spec/models/note_spec.rb'
+- './ee/spec/models/packages/package_file_spec.rb'
+- './ee/spec/models/path_lock_spec.rb'
+- './ee/spec/models/plan_spec.rb'
+- './ee/spec/models/preloaders/environments/protected_environment_preloader_spec.rb'
+- './ee/spec/models/productivity_analytics_spec.rb'
+- './ee/spec/models/project_alias_spec.rb'
+- './ee/spec/models/project_ci_cd_setting_spec.rb'
+- './ee/spec/models/project_feature_spec.rb'
+- './ee/spec/models/project_import_data_spec.rb'
+- './ee/spec/models/project_import_state_spec.rb'
+- './ee/spec/models/project_member_spec.rb'
+- './ee/spec/models/project_repository_state_spec.rb'
+- './ee/spec/models/project_security_setting_spec.rb'
+- './ee/spec/models/project_spec.rb'
+- './ee/spec/models/project_team_spec.rb'
+- './ee/spec/models/protected_branch/required_code_owners_section_spec.rb'
+- './ee/spec/models/protected_branch/unprotect_access_level_spec.rb'
+- './ee/spec/models/protected_environment/deploy_access_level_spec.rb'
+- './ee/spec/models/protected_environments/approval_rule_spec.rb'
+- './ee/spec/models/protected_environments/approval_summary_spec.rb'
+- './ee/spec/models/protected_environment_spec.rb'
+- './ee/spec/models/push_rule_spec.rb'
+- './ee/spec/models/release_highlight_spec.rb'
+- './ee/spec/models/remote_mirror_spec.rb'
+- './ee/spec/models/repository_spec.rb'
+- './ee/spec/models/requirements_management/requirement_spec.rb'
+- './ee/spec/models/requirements_management/test_report_spec.rb'
+- './ee/spec/models/resource_iteration_event_spec.rb'
+- './ee/spec/models/resource_weight_event_spec.rb'
+- './ee/spec/models/saml_group_link_spec.rb'
+- './ee/spec/models/saml_provider_spec.rb'
+- './ee/spec/models/sbom/component_spec.rb'
+- './ee/spec/models/sbom/component_version_spec.rb'
+- './ee/spec/models/sbom/occurrence_spec.rb'
+- './ee/spec/models/sbom/source_spec.rb'
+- './ee/spec/models/sca/license_compliance_spec.rb'
+- './ee/spec/models/sca/license_policy_spec.rb'
+- './ee/spec/models/scim_identity_spec.rb'
+- './ee/spec/models/scim_oauth_access_token_spec.rb'
+- './ee/spec/models/scoped_label_set_spec.rb'
+- './ee/spec/models/security/finding_spec.rb'
+- './ee/spec/models/security/orchestration_policy_configuration_spec.rb'
+- './ee/spec/models/security/orchestration_policy_rule_schedule_spec.rb'
+- './ee/spec/models/security/scan_spec.rb'
+- './ee/spec/models/security/training_provider_spec.rb'
+- './ee/spec/models/security/training_spec.rb'
+- './ee/spec/models/slack_integration_spec.rb'
+- './ee/spec/models/snippet_repository_spec.rb'
+- './ee/spec/models/snippet_spec.rb'
+- './ee/spec/models/software_license_policy_spec.rb'
+- './ee/spec/models/software_license_spec.rb'
+- './ee/spec/models/status_page/project_setting_spec.rb'
+- './ee/spec/models/status_page/published_incident_spec.rb'
+- './ee/spec/models/storage_shard_spec.rb'
+- './ee/spec/models/uploads/local_spec.rb'
+- './ee/spec/models/upload_spec.rb'
+- './ee/spec/models/user_detail_spec.rb'
+- './ee/spec/models/user_permission_export_upload_spec.rb'
+- './ee/spec/models/user_preference_spec.rb'
+- './ee/spec/models/users_security_dashboard_project_spec.rb'
+- './ee/spec/models/visible_approvable_spec.rb'
+- './ee/spec/models/vulnerabilities/export_spec.rb'
+- './ee/spec/models/vulnerabilities/external_issue_link_spec.rb'
+- './ee/spec/models/vulnerabilities/feedback_spec.rb'
+- './ee/spec/models/vulnerabilities/finding/evidence_spec.rb'
+- './ee/spec/models/vulnerabilities/finding_identifier_spec.rb'
+- './ee/spec/models/vulnerabilities/finding_link_spec.rb'
+- './ee/spec/models/vulnerabilities/finding_pipeline_spec.rb'
+- './ee/spec/models/vulnerabilities/finding_remediation_spec.rb'
+- './ee/spec/models/vulnerabilities/finding_signature_spec.rb'
+- './ee/spec/models/vulnerabilities/finding_spec.rb'
+- './ee/spec/models/vulnerabilities/flag_spec.rb'
+- './ee/spec/models/vulnerabilities/historical_statistic_spec.rb'
+- './ee/spec/models/vulnerabilities/identifier_spec.rb'
+- './ee/spec/models/vulnerabilities/issue_link_spec.rb'
+- './ee/spec/models/vulnerabilities/merge_request_link_spec.rb'
+- './ee/spec/models/vulnerabilities/projects_grade_spec.rb'
+- './ee/spec/models/vulnerabilities/read_spec.rb'
+- './ee/spec/models/vulnerabilities/remediation_spec.rb'
+- './ee/spec/models/vulnerabilities/scanner_spec.rb'
+- './ee/spec/models/vulnerabilities/stat_diff_spec.rb'
+- './ee/spec/models/vulnerabilities/state_transition_spec.rb'
+- './ee/spec/models/vulnerabilities/statistic_spec.rb'
+- './ee/spec/models/vulnerability_user_mention_spec.rb'
+- './ee/spec/models/weight_note_spec.rb'
+- './ee/spec/models/work_item_spec.rb'
+- './ee/spec/models/work_items/widgets/verification_status_spec.rb'
+- './ee/spec/policies/approval_merge_request_rule_policy_spec.rb'
+- './ee/spec/policies/approval_project_rule_policy_spec.rb'
+- './ee/spec/policies/approval_state_policy_spec.rb'
+- './ee/spec/policies/app_sec/fuzzing/coverage/corpus_policy_spec.rb'
+- './ee/spec/policies/award_emoji_policy_spec.rb'
+- './ee/spec/policies/base_policy_spec.rb'
+- './ee/spec/policies/ci/build_policy_spec.rb'
+- './ee/spec/policies/ci/minutes/namespace_monthly_usage_policy_spec.rb'
+- './ee/spec/policies/clusters/instance_policy_spec.rb'
+- './ee/spec/policies/compliance_management/framework_policy_spec.rb'
+- './ee/spec/policies/dast/branch_policy_spec.rb'
+- './ee/spec/policies/dast/profile_policy_spec.rb'
+- './ee/spec/policies/dast/profile_schedule_policy_spec.rb'
+- './ee/spec/policies/dast_scanner_profile_policy_spec.rb'
+- './ee/spec/policies/dast_site_profile_policy_spec.rb'
+- './ee/spec/policies/dast_site_validation_policy_spec.rb'
+- './ee/spec/policies/ee/ci/runner_policy_spec.rb'
+- './ee/spec/policies/ee/namespaces/user_namespace_policy_spec.rb'
+- './ee/spec/policies/ee/readonly_abilities_spec.rb'
+- './ee/spec/policies/environment_policy_spec.rb'
+- './ee/spec/policies/epic_policy_spec.rb'
+- './ee/spec/policies/event_policy_spec.rb'
+- './ee/spec/policies/geo_node_policy_spec.rb'
+- './ee/spec/policies/geo/registry_policy_spec.rb'
+- './ee/spec/policies/global_policy_spec.rb'
+- './ee/spec/policies/group_hook_policy_spec.rb'
+- './ee/spec/policies/group_policy_spec.rb'
+- './ee/spec/policies/identity_provider_policy_spec.rb'
+- './ee/spec/policies/incident_management/oncall_rotation_policy_spec.rb'
+- './ee/spec/policies/incident_management/oncall_schedule_policy_spec.rb'
+- './ee/spec/policies/incident_management/oncall_shift_policy_spec.rb'
+- './ee/spec/policies/instance_security_dashboard_policy_spec.rb'
+- './ee/spec/policies/issuable_policy_spec.rb'
+- './ee/spec/policies/issue_policy_spec.rb'
+- './ee/spec/policies/merge_request_policy_spec.rb'
+- './ee/spec/policies/note_policy_spec.rb'
+- './ee/spec/policies/path_lock_policy_spec.rb'
+- './ee/spec/policies/project_policy_spec.rb'
+- './ee/spec/policies/project_snippet_policy_spec.rb'
+- './ee/spec/policies/protected_branch_policy_spec.rb'
+- './ee/spec/policies/requirements_management/requirement_policy_spec.rb'
+- './ee/spec/policies/saml_provider_policy_spec.rb'
+- './ee/spec/policies/security/scan_policy_spec.rb'
+- './ee/spec/policies/user_policy_spec.rb'
+- './ee/spec/policies/vulnerabilities/export_policy_spec.rb'
+- './ee/spec/policies/vulnerabilities/external_issue_link_policy_spec.rb'
+- './ee/spec/policies/vulnerabilities/feedback_policy_spec.rb'
+- './ee/spec/policies/vulnerabilities/issue_link_policy_spec.rb'
+- './ee/spec/policies/vulnerabilities/scanner_policy_spec.rb'
+- './ee/spec/policies/vulnerability_policy_spec.rb'
+- './ee/spec/presenters/analytics/cycle_analytics/stage_presenter_spec.rb'
+- './ee/spec/presenters/approval_rule_presenter_spec.rb'
+- './ee/spec/presenters/audit_event_presenter_spec.rb'
+- './ee/spec/presenters/ci/build_presenter_spec.rb'
+- './ee/spec/presenters/ci/build_runner_presenter_spec.rb'
+- './ee/spec/presenters/ci/minutes/usage_presenter_spec.rb'
+- './ee/spec/presenters/ci/pipeline_presenter_spec.rb'
+- './ee/spec/presenters/dast/site_profile_presenter_spec.rb'
+- './ee/spec/presenters/ee/blob_presenter_spec.rb'
+- './ee/spec/presenters/ee/clusters/cluster_presenter_spec.rb'
+- './ee/spec/presenters/ee/instance_clusterable_presenter_spec.rb'
+- './ee/spec/presenters/ee/issue_presenter_spec.rb'
+- './ee/spec/presenters/ee/projects/security/configuration_presenter_spec.rb'
+- './ee/spec/presenters/epic_issue_presenter_spec.rb'
+- './ee/spec/presenters/epic_presenter_spec.rb'
+- './ee/spec/presenters/group_clusterable_presenter_spec.rb'
+- './ee/spec/presenters/group_member_presenter_spec.rb'
+- './ee/spec/presenters/label_presenter_spec.rb'
+- './ee/spec/presenters/merge_request_approver_presenter_spec.rb'
+- './ee/spec/presenters/merge_request_presenter_spec.rb'
+- './ee/spec/presenters/project_clusterable_presenter_spec.rb'
+- './ee/spec/presenters/project_member_presenter_spec.rb'
+- './ee/spec/presenters/security/scan_presenter_spec.rb'
+- './ee/spec/presenters/subscription_presenter_spec.rb'
+- './ee/spec/presenters/subscriptions/new_plan_presenter_spec.rb'
+- './ee/spec/presenters/vulnerabilities/finding_presenter_spec.rb'
+- './ee/spec/presenters/vulnerability_presenter_spec.rb'
+- './ee/spec/presenters/web_hooks/group/hook_presenter_spec.rb'
+- './ee/spec/replicators/geo/ci_secure_file_replicator_spec.rb'
+- './ee/spec/replicators/geo/group_wiki_repository_replicator_spec.rb'
+- './ee/spec/replicators/geo/job_artifact_replicator_spec.rb'
+- './ee/spec/replicators/geo/lfs_object_replicator_spec.rb'
+- './ee/spec/replicators/geo/merge_request_diff_replicator_spec.rb'
+- './ee/spec/replicators/geo/package_file_replicator_spec.rb'
+- './ee/spec/replicators/geo/pages_deployment_replicator_spec.rb'
+- './ee/spec/replicators/geo/pipeline_artifact_replicator_spec.rb'
+- './ee/spec/replicators/geo/pipeline_replicator_spec.rb'
+- './ee/spec/replicators/geo/snippet_repository_replicator_spec.rb'
+- './ee/spec/replicators/geo/terraform_state_version_replicator_spec.rb'
+- './ee/spec/replicators/geo/upload_replicator_spec.rb'
+- './ee/spec/requests/admin/audit_events_spec.rb'
+- './ee/spec/requests/admin/credentials_controller_spec.rb'
+- './ee/spec/requests/admin/geo/nodes_controller_spec.rb'
+- './ee/spec/requests/admin/geo/replicables_controller_spec.rb'
+- './ee/spec/requests/admin/subscriptions_controller_spec.rb'
+- './ee/spec/requests/admin/user_permission_exports_controller_spec.rb'
+- './ee/spec/requests/admin/users_controller_spec.rb'
+- './ee/spec/requests/api/analytics/code_review_analytics_spec.rb'
+- './ee/spec/requests/api/analytics/group_activity_analytics_spec.rb'
+- './ee/spec/requests/api/analytics/project_deployment_frequency_spec.rb'
+- './ee/spec/requests/api/api_spec.rb'
+- './ee/spec/requests/api/audit_events_spec.rb'
+- './ee/spec/requests/api/award_emoji_spec.rb'
+- './ee/spec/requests/api/boards_spec.rb'
+- './ee/spec/requests/api/branches_spec.rb'
+- './ee/spec/requests/api/captcha_check_spec.rb'
+- './ee/spec/requests/api/ci/jobs_spec.rb'
+- './ee/spec/requests/api/ci/minutes_spec.rb'
+- './ee/spec/requests/api/ci/pipelines_spec.rb'
+- './ee/spec/requests/api/ci/runner/jobs_put_spec.rb'
+- './ee/spec/requests/api/ci/runner/jobs_trace_spec.rb'
+- './ee/spec/requests/api/ci/runner_spec.rb'
+- './ee/spec/requests/api/ci/triggers_spec.rb'
+- './ee/spec/requests/api/ci/variables_spec.rb'
+- './ee/spec/requests/api/commits_spec.rb'
+- './ee/spec/requests/api/dependencies_spec.rb'
+- './ee/spec/requests/api/deployments_spec.rb'
+- './ee/spec/requests/api/discussions_spec.rb'
+- './ee/spec/requests/api/dora/metrics_spec.rb'
+- './ee/spec/requests/api/elasticsearch_indexed_namespaces_spec.rb'
+- './ee/spec/requests/api/epic_issues_spec.rb'
+- './ee/spec/requests/api/epic_links_spec.rb'
+- './ee/spec/requests/api/epics_spec.rb'
+- './ee/spec/requests/api/experiments_spec.rb'
+- './ee/spec/requests/api/features_spec.rb'
+- './ee/spec/requests/api/files_spec.rb'
+- './ee/spec/requests/api/geo_nodes_spec.rb'
+- './ee/spec/requests/api/geo_replication_spec.rb'
+- './ee/spec/requests/api/geo_spec.rb'
+- './ee/spec/requests/api/graphql/analytics/devops_adoption/enabled_namespaces_spec.rb'
+- './ee/spec/requests/api/graphql/app_sec/fuzzing/api/ci_configuration_type_spec.rb'
+- './ee/spec/requests/api/graphql/app_sec/fuzzing/coverage/corpus_type_spec.rb'
+- './ee/spec/requests/api/graphql/audit_events/streaming/headers/create_spec.rb'
+- './ee/spec/requests/api/graphql/audit_events/streaming/headers/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/audit_events/streaming/headers/update_spec.rb'
+- './ee/spec/requests/api/graphql/boards/board_list_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/board_lists_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/boards_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/epic_boards_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/epic_list_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/epic_lists_query_spec.rb'
+- './ee/spec/requests/api/graphql/boards/epic_lists/update_spec.rb'
+- './ee/spec/requests/api/graphql/ci/minutes/usage_spec.rb'
+- './ee/spec/requests/api/graphql/ci/runner_spec.rb'
+- './ee/spec/requests/api/graphql/ci/runners_spec.rb'
+- './ee/spec/requests/api/graphql/compliance_management/merge_requests/compliance_violations_spec.rb'
+- './ee/spec/requests/api/graphql/current_user/groups_query_spec.rb'
+- './ee/spec/requests/api/graphql/current_user/todos_query_spec.rb'
+- './ee/spec/requests/api/graphql/dora/dora_spec.rb'
+- './ee/spec/requests/api/graphql/epics/epic_resolver_spec.rb'
+- './ee/spec/requests/api/graphql/geo/geo_node_spec.rb'
+- './ee/spec/requests/api/graphql/geo/registries_spec.rb'
+- './ee/spec/requests/api/graphql/group/ci_cd_settings_spec.rb'
+- './ee/spec/requests/api/graphql/group/dast_profile_schedule_spec.rb'
+- './ee/spec/requests/api/graphql/group/epic/epic_aggregate_query_spec.rb'
+- './ee/spec/requests/api/graphql/group/epic/epic_ancestors_spec.rb'
+- './ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb'
+- './ee/spec/requests/api/graphql/group/epic/notes_spec.rb'
+- './ee/spec/requests/api/graphql/group/epics_spec.rb'
+- './ee/spec/requests/api/graphql/group/external_audit_event_destinations_spec.rb'
+- './ee/spec/requests/api/graphql/group_query_spec.rb'
+- './ee/spec/requests/api/graphql/incident_management/issuable_resource_links_spec.rb'
+- './ee/spec/requests/api/graphql/instance_security_dashboard_spec.rb'
+- './ee/spec/requests/api/graphql/iterations/cadences_spec.rb'
+- './ee/spec/requests/api/graphql/iterations/iterations_spec.rb'
+- './ee/spec/requests/api/graphql/iteration_spec.rb'
+- './ee/spec/requests/api/graphql/merge_request_reviewer_spec.rb'
+- './ee/spec/requests/api/graphql/merge_requests/approval_state_spec.rb'
+- './ee/spec/requests/api/graphql/milestone_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/bulk_enable_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/disable_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/enable_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epic_boards/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epic_boards/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epic_boards/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epic_lists/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epic_lists/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/epics/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/lists/update_limit_metrics_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/boards/update_epic_user_preferences_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/ci/namespace_ci_cd_settings_update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_on_demand_scans/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast/profiles/delete_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast/profiles/run_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast/profiles/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_scanner_profiles/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_scanner_profiles/delete_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_scanner_profiles/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_site_profiles/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_site_profiles/delete_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_site_profiles/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_site_tokens/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_site_validations/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/dast_site_validations/revoke_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/epics/add_issue_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/epics/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/epics/set_subscription_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/epics/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/epic_tree/reorder_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/gitlab_subscriptions/activate_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/escalation_policy/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/escalation_policy/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/escalation_policy/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/issuable_resource_link/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/issuable_resource_link/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_rotation/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_rotation/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_schedule/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_schedule/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_schedule/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/issues/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/issues/promote_to_epic_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/issues/set_epic_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/issues/set_escalation_policy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/issues/set_weight_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/issues/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/iterations/cadences/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/iterations/cadences/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/iterations/cadences/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/iterations/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/iterations/delete_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/iterations/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/notes/create/note_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/projects/lock_path_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/projects/set_compliance_framework_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/quality_management/test_cases/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/releases/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/releases/update_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/requirements_management/create_requirement_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/requirements_management/export_requirements_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/requirements_management/update_requirement_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/security_finding/create_issue_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/security_policy/assign_security_policy_project_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/timelogs/create_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/users/abuse/namespace_bans/destroy_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/vulnerabilities/finding_dismiss_spec.rb'
+- './ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb'
+- './ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb'
+- './ee/spec/requests/api/graphql/namespace/projects_spec.rb'
+- './ee/spec/requests/api/graphql/project/alert_management/http_integrations_spec.rb'
+- './ee/spec/requests/api/graphql/project/alert_management/integrations_spec.rb'
+- './ee/spec/requests/api/graphql/project/alert_management/payload_fields_spec.rb'
+- './ee/spec/requests/api/graphql/project/code_coverage_summary_spec.rb'
+- './ee/spec/requests/api/graphql/project/compliance_frameworks_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_profile_schedule_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_profile_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_profiles_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_scanner_profiles_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_site_profile_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_site_profiles_spec.rb'
+- './ee/spec/requests/api/graphql/project/dast_site_validations_spec.rb'
+- './ee/spec/requests/api/graphql/project/incident_management/escalation_policies_spec.rb'
+- './ee/spec/requests/api/graphql/project/incident_management/escalation_policy/rules_spec.rb'
+- './ee/spec/requests/api/graphql/project/incident_management/oncall_participants_spec.rb'
+- './ee/spec/requests/api/graphql/project/incident_management/oncall_schedules_spec.rb'
+- './ee/spec/requests/api/graphql/project/incident_management/oncall_shifts_spec.rb'
+- './ee/spec/requests/api/graphql/project/issues_spec.rb'
+- './ee/spec/requests/api/graphql/project/merge_requests_spec.rb'
+- './ee/spec/requests/api/graphql/project/path_locks_spec.rb'
+- './ee/spec/requests/api/graphql/project/pipeline/code_quality_reports_spec.rb'
+- './ee/spec/requests/api/graphql/project/pipeline/dast_profile_spec.rb'
+- './ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb'
+- './ee/spec/requests/api/graphql/project/pipeline/security_report_finding_spec.rb'
+- './ee/spec/requests/api/graphql/project/pipeline/security_report_summary_spec.rb'
+- './ee/spec/requests/api/graphql/project/push_rules_spec.rb'
+- './ee/spec/requests/api/graphql/project/requirements_management/requirement_counts_spec.rb'
+- './ee/spec/requests/api/graphql/project/requirements_management/requirements_spec.rb'
+- './ee/spec/requests/api/graphql/project/requirements_management/test_reports_spec.rb'
+- './ee/spec/requests/api/graphql/projects/compliance_frameworks_spec.rb'
+- './ee/spec/requests/api/graphql/project/security_orchestration/scan_result_policy_spec.rb'
+- './ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb'
+- './ee/spec/requests/api/graphql/project/work_items_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/description_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/details_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/identifiers_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/issue_links_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/primary_identifier_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/scanner_spec.rb'
+- './ee/spec/requests/api/graphql/vulnerabilities/sort_spec.rb'
+- './ee/spec/requests/api/graphql/work_item_spec.rb'
+- './ee/spec/requests/api/group_boards_spec.rb'
+- './ee/spec/requests/api/group_clusters_spec.rb'
+- './ee/spec/requests/api/group_hooks_spec.rb'
+- './ee/spec/requests/api/group_milestones_spec.rb'
+- './ee/spec/requests/api/group_push_rule_spec.rb'
+- './ee/spec/requests/api/group_repository_storage_moves_spec.rb'
+- './ee/spec/requests/api/groups_spec.rb'
+- './ee/spec/requests/api/group_variables_spec.rb'
+- './ee/spec/requests/api/integrations_spec.rb'
+- './ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb'
+- './ee/spec/requests/api/internal/base_spec.rb'
+- './ee/spec/requests/api/internal/kubernetes_spec.rb'
+- './ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb'
+- './ee/spec/requests/api/invitations_spec.rb'
+- './ee/spec/requests/api/issue_links_spec.rb'
+- './ee/spec/requests/api/issues_spec.rb'
+- './ee/spec/requests/api/iterations_spec.rb'
+- './ee/spec/requests/api/ldap_group_links_spec.rb'
+- './ee/spec/requests/api/ldap_spec.rb'
+- './ee/spec/requests/api/license_spec.rb'
+- './ee/spec/requests/api/managed_licenses_spec.rb'
+- './ee/spec/requests/api/markdown_golden_master_spec.rb'
+- './ee/spec/requests/api/members_spec.rb'
+- './ee/spec/requests/api/merge_request_approval_rules_spec.rb'
+- './ee/spec/requests/api/merge_request_approval_settings_spec.rb'
+- './ee/spec/requests/api/merge_request_approvals_spec.rb'
+- './ee/spec/requests/api/merge_requests_spec.rb'
+- './ee/spec/requests/api/merge_trains_spec.rb'
+- './ee/spec/requests/api/namespaces_spec.rb'
+- './ee/spec/requests/api/notes_spec.rb'
+- './ee/spec/requests/api/project_aliases_spec.rb'
+- './ee/spec/requests/api/project_approval_rules_spec.rb'
+- './ee/spec/requests/api/project_approval_settings_spec.rb'
+- './ee/spec/requests/api/project_approvals_spec.rb'
+- './ee/spec/requests/api/project_clusters_spec.rb'
+- './ee/spec/requests/api/project_import_spec.rb'
+- './ee/spec/requests/api/project_milestones_spec.rb'
+- './ee/spec/requests/api/project_mirror_spec.rb'
+- './ee/spec/requests/api/project_push_rule_spec.rb'
+- './ee/spec/requests/api/project_snapshots_spec.rb'
+- './ee/spec/requests/api/projects_spec.rb'
+- './ee/spec/requests/api/protected_branches_spec.rb'
+- './ee/spec/requests/api/protected_environments_spec.rb'
+- './ee/spec/requests/api/protected_tags_spec.rb'
+- './ee/spec/requests/api/related_epic_links_spec.rb'
+- './ee/spec/requests/api/releases_spec.rb'
+- './ee/spec/requests/api/repositories_spec.rb'
+- './ee/spec/requests/api/resource_iteration_events_spec.rb'
+- './ee/spec/requests/api/resource_label_events_spec.rb'
+- './ee/spec/requests/api/resource_weight_events_spec.rb'
+- './ee/spec/requests/api/saml_group_links_spec.rb'
+- './ee/spec/requests/api/scim_spec.rb'
+- './ee/spec/requests/api/search_spec.rb'
+- './ee/spec/requests/api/settings_spec.rb'
+- './ee/spec/requests/api/status_checks_spec.rb'
+- './ee/spec/requests/api/submodules_spec.rb'
+- './ee/spec/requests/api/templates_spec.rb'
+- './ee/spec/requests/api/todos_spec.rb'
+- './ee/spec/requests/api/usage_data_spec.rb'
+- './ee/spec/requests/api/users_spec.rb'
+- './ee/spec/requests/api/v3/github_spec.rb'
+- './ee/spec/requests/api/visual_review_discussions_spec.rb'
+- './ee/spec/requests/api/vulnerabilities_spec.rb'
+- './ee/spec/requests/api/vulnerability_exports_spec.rb'
+- './ee/spec/requests/api/vulnerability_findings_spec.rb'
+- './ee/spec/requests/api/vulnerability_issue_links_spec.rb'
+- './ee/spec/requests/api/wikis_spec.rb'
+- './ee/spec/requests/callout_spec.rb'
+- './ee/spec/requests/customers_dot/proxy_controller_spec.rb'
+- './ee/spec/requests/ee/groups/autocomplete_sources_spec.rb'
+- './ee/spec/requests/ee/groups/settings/repository_controller_spec.rb'
+- './ee/spec/requests/ee/projects/deploy_tokens_controller_spec.rb'
+- './ee/spec/requests/ee/projects/environments_controller_spec.rb'
+- './ee/spec/requests/ee/projects/service_desk_controller_spec.rb'
+- './ee/spec/requests/git_http_geo_spec.rb'
+- './ee/spec/requests/git_http_spec.rb'
+- './ee/spec/requests/groups/analytics/devops_adoption_controller_spec.rb'
+- './ee/spec/requests/groups/audit_events_spec.rb'
+- './ee/spec/requests/groups/clusters_controller_spec.rb'
+- './ee/spec/requests/groups/compliance_frameworks_spec.rb'
+- './ee/spec/requests/groups/contribution_analytics_spec.rb'
+- './ee/spec/requests/groups_controller_spec.rb'
+- './ee/spec/requests/groups/epics/epic_links_controller_spec.rb'
+- './ee/spec/requests/groups/epics/related_epic_links_controller_spec.rb'
+- './ee/spec/requests/groups/feature_discovery_moments_spec.rb'
+- './ee/spec/requests/groups/group_members_controller_spec.rb'
+- './ee/spec/requests/groups/hook_logs_controller_spec.rb'
+- './ee/spec/requests/groups/labels_spec.rb'
+- './ee/spec/requests/groups/protected_environments_controller_spec.rb'
+- './ee/spec/requests/groups/roadmap_controller_spec.rb'
+- './ee/spec/requests/groups/security/credentials_controller_spec.rb'
+- './ee/spec/requests/groups/settings/reporting_controller_spec.rb'
+- './ee/spec/requests/groups/usage_quotas_spec.rb'
+- './ee/spec/requests/jwt_controller_spec.rb'
+- './ee/spec/requests/lfs_http_spec.rb'
+- './ee/spec/requests/lfs_locks_api_spec.rb'
+- './ee/spec/requests/omniauth_kerberos_spec.rb'
+- './ee/spec/requests/projects/analytics/code_reviews_controller_spec.rb'
+- './ee/spec/requests/projects/audit_events_spec.rb'
+- './ee/spec/requests/projects/incidents_controller_spec.rb'
+- './ee/spec/requests/projects/issue_feature_flags_controller_spec.rb'
+- './ee/spec/requests/projects/issues_controller_spec.rb'
+- './ee/spec/requests/projects/merge_requests_controller_spec.rb'
+- './ee/spec/requests/projects/mirrors_controller_spec.rb'
+- './ee/spec/requests/projects/on_demand_scans_controller_spec.rb'
+- './ee/spec/requests/projects/pipelines_controller_spec.rb'
+- './ee/spec/requests/projects/pipelines/email_campaigns_controller_spec.rb'
+- './ee/spec/requests/projects/requirements_management/requirements_controller_spec.rb'
+- './ee/spec/requests/projects/security/corpus_management_controller_spec.rb'
+- './ee/spec/requests/projects/security/dast_configuration_controller_spec.rb'
+- './ee/spec/requests/projects/security/dast_profiles_controller_spec.rb'
+- './ee/spec/requests/projects/security/dast_scanner_profiles_controller_spec.rb'
+- './ee/spec/requests/projects/security/dast_site_profiles_controller_spec.rb'
+- './ee/spec/requests/projects/security/policies_controller_spec.rb'
+- './ee/spec/requests/projects/security/scanned_resources_controller_spec.rb'
+- './ee/spec/requests/projects/settings/access_tokens_controller_spec.rb'
+- './ee/spec/requests/rack_attack_global_spec.rb'
+- './ee/spec/requests/rack_attack_spec.rb'
+- './ee/spec/requests/repositories/git_http_controller_spec.rb'
+- './ee/spec/requests/search_controller_spec.rb'
+- './ee/spec/requests/smartcard_controller_spec.rb'
+- './ee/spec/requests/trial_registrations_controller_spec.rb'
+- './ee/spec/requests/user_activity_spec.rb'
+- './ee/spec/routing/admin_routing_spec.rb'
+- './ee/spec/routing/git_http_routing_spec.rb'
+- './ee/spec/routing/group_routing_spec.rb'
+- './ee/spec/routing/groups/cadences_routing_spec.rb'
+- './ee/spec/routing/operations_routing_spec.rb'
+- './ee/spec/routing/project_routing_spec.rb'
+- './ee/spec/routing/projects/security/configuration_controller_routing_spec.rb'
+- './ee/spec/routing/security_routing_spec.rb'
+- './ee/spec/routing/uploads_routing_spec.rb'
+- './ee/spec/routing/user_routing_spec.rb'
+- './ee/spec/routing/webhook_routes_spec.rb'
+- './ee/spec/serializers/analytics/cycle_analytics/event_entity_spec.rb'
+- './ee/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb'
+- './ee/spec/serializers/analytics/cycle_analytics/value_stream_errors_serializer_spec.rb'
+- './ee/spec/serializers/audit_event_entity_spec.rb'
+- './ee/spec/serializers/audit_event_serializer_spec.rb'
+- './ee/spec/serializers/autocomplete/group_entity_spec.rb'
+- './ee/spec/serializers/autocomplete/group_serializer_spec.rb'
+- './ee/spec/serializers/blocking_merge_request_entity_spec.rb'
+- './ee/spec/serializers/board_serializer_spec.rb'
+- './ee/spec/serializers/clusters/deployment_entity_spec.rb'
+- './ee/spec/serializers/clusters/environment_entity_spec.rb'
+- './ee/spec/serializers/clusters/environment_serializer_spec.rb'
+- './ee/spec/serializers/dashboard_environment_entity_spec.rb'
+- './ee/spec/serializers/dashboard_environments_project_entity_spec.rb'
+- './ee/spec/serializers/dashboard_environments_serializer_spec.rb'
+- './ee/spec/serializers/dashboard_operations_project_entity_spec.rb'
+- './ee/spec/serializers/dependency_entity_spec.rb'
+- './ee/spec/serializers/dependency_list_entity_spec.rb'
+- './ee/spec/serializers/dependency_list_serializer_spec.rb'
+- './ee/spec/serializers/ee/admin/user_entity_spec.rb'
+- './ee/spec/serializers/ee/blob_entity_spec.rb'
+- './ee/spec/serializers/ee/board_simple_entity_spec.rb'
+- './ee/spec/serializers/ee/build_details_entity_spec.rb'
+- './ee/spec/serializers/ee/ci/job_entity_spec.rb'
+- './ee/spec/serializers/ee/ci/pipeline_entity_spec.rb'
+- './ee/spec/serializers/ee/deployment_entity_spec.rb'
+- './ee/spec/serializers/ee/environment_serializer_spec.rb'
+- './ee/spec/serializers/ee/evidences/release_entity_spec.rb'
+- './ee/spec/serializers/ee/group_child_entity_spec.rb'
+- './ee/spec/serializers/ee/issue_board_entity_spec.rb'
+- './ee/spec/serializers/ee/issue_entity_spec.rb'
+- './ee/spec/serializers/ee/issue_sidebar_basic_entity_spec.rb'
+- './ee/spec/serializers/ee/issue_sidebar_extras_entity_spec.rb'
+- './ee/spec/serializers/ee/merge_request_poll_cached_widget_entity_spec.rb'
+- './ee/spec/serializers/ee/note_entity_spec.rb'
+- './ee/spec/serializers/ee/user_serializer_spec.rb'
+- './ee/spec/serializers/environment_entity_spec.rb'
+- './ee/spec/serializers/epic_entity_spec.rb'
+- './ee/spec/serializers/epic_note_entity_spec.rb'
+- './ee/spec/serializers/epics/related_epic_entity_spec.rb'
+- './ee/spec/serializers/evidences/build_artifact_entity_spec.rb'
+- './ee/spec/serializers/evidences/evidence_entity_spec.rb'
+- './ee/spec/serializers/fork_namespace_entity_spec.rb'
+- './ee/spec/serializers/geo_project_registry_entity_spec.rb'
+- './ee/spec/serializers/group_vulnerability_autocomplete_entity_spec.rb'
+- './ee/spec/serializers/incident_management/escalation_policy_entity_spec.rb'
+- './ee/spec/serializers/incident_management/oncall_schedule_entity_spec.rb'
+- './ee/spec/serializers/integrations/field_entity_spec.rb'
+- './ee/spec/serializers/integrations/jira_serializers/issue_detail_entity_spec.rb'
+- './ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
+- './ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
+- './ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
+- './ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
+- './ee/spec/serializers/issue_serializer_spec.rb'
+- './ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
+- './ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
+- './ee/spec/serializers/license_compliance/comparer_entity_spec.rb'
+- './ee/spec/serializers/license_entity_spec.rb'
+- './ee/spec/serializers/licenses_list_entity_spec.rb'
+- './ee/spec/serializers/licenses_list_serializer_spec.rb'
+- './ee/spec/serializers/linked_feature_flag_issue_entity_spec.rb'
+- './ee/spec/serializers/member_entity_spec.rb'
+- './ee/spec/serializers/member_user_entity_spec.rb'
+- './ee/spec/serializers/merge_request_poll_widget_entity_spec.rb'
+- './ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
+- './ee/spec/serializers/merge_request_widget_entity_spec.rb'
+- './ee/spec/serializers/metrics_report_metric_entity_spec.rb'
+- './ee/spec/serializers/metrics_reports_comparer_entity_spec.rb'
+- './ee/spec/serializers/pipeline_serializer_spec.rb'
+- './ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb'
+- './ee/spec/serializers/project_mirror_entity_spec.rb'
+- './ee/spec/serializers/protected_environments/deploy_access_level_entity_spec.rb'
+- './ee/spec/serializers/protected_environments/entity_spec.rb'
+- './ee/spec/serializers/scim_oauth_access_token_entity_spec.rb'
+- './ee/spec/serializers/security/license_policy_entity_spec.rb'
+- './ee/spec/serializers/security/vulnerability_report_data_entity_spec.rb'
+- './ee/spec/serializers/security/vulnerability_report_data_serializer_spec.rb'
+- './ee/spec/serializers/status_page/incident_comment_entity_spec.rb'
+- './ee/spec/serializers/status_page/incident_entity_spec.rb'
+- './ee/spec/serializers/status_page/incident_serializer_spec.rb'
+- './ee/spec/serializers/status_page/renderer_spec.rb'
+- './ee/spec/serializers/storage_shard_entity_spec.rb'
+- './ee/spec/serializers/test_reports_comparer_entity_spec.rb'
+- './ee/spec/serializers/test_reports_comparer_serializer_spec.rb'
+- './ee/spec/serializers/test_suite_comparer_entity_spec.rb'
+- './ee/spec/serializers/user_analytics_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/feedback_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/finding_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/finding_reports_comparer_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/finding_serializer_spec.rb'
+- './ee/spec/serializers/vulnerabilities/identifier_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/request_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/response_entity_spec.rb'
+- './ee/spec/serializers/vulnerabilities/scanner_entity_spec.rb'
+- './ee/spec/serializers/vulnerability_entity_spec.rb'
+- './ee/spec/serializers/vulnerability_note_entity_spec.rb'
+- './ee/spec/services/admin/email_service_spec.rb'
+- './ee/spec/services/alert_management/extract_alert_payload_fields_service_spec.rb'
+- './ee/spec/services/alert_management/process_prometheus_alert_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/aggregator_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/consistency_check_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/data_loader_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/stages/create_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/stages/delete_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/stages/update_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/value_streams/create_service_spec.rb'
+- './ee/spec/services/analytics/cycle_analytics/value_streams/update_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/enabled_namespaces/bulk_delete_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/enabled_namespaces/bulk_find_or_create_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/enabled_namespaces/create_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/enabled_namespaces/delete_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/enabled_namespaces/find_or_create_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/snapshots/calculate_and_save_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/snapshots/create_service_spec.rb'
+- './ee/spec/services/analytics/devops_adoption/snapshots/update_service_spec.rb'
+- './ee/spec/services/applications/create_service_spec.rb'
+- './ee/spec/services/application_settings/update_service_spec.rb'
+- './ee/spec/services/approval_rules/create_service_spec.rb'
+- './ee/spec/services/approval_rules/finalize_service_spec.rb'
+- './ee/spec/services/approval_rules/merge_request_rule_destroy_service_spec.rb'
+- './ee/spec/services/approval_rules/params_filtering_service_spec.rb'
+- './ee/spec/services/approval_rules/project_rule_destroy_service_spec.rb'
+- './ee/spec/services/approval_rules/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/builds/associate_service_spec.rb'
+- './ee/spec/services/app_sec/dast/pipelines/find_latest_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profiles/audit/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profiles/build_config_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profile_schedules/audit/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profiles/create_associations_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profiles/create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profiles/destroy_service_spec.rb'
+- './ee/spec/services/app_sec/dast/profiles/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scan_configs/build_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scan_configs/fetch_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scanner_profiles/create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scanner_profiles/destroy_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scanner_profiles/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scans/create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/scans/run_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_profiles/audit/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_profiles/create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_profiles/destroy_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_profile_secret_variables/create_or_update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_profile_secret_variables/destroy_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_profiles/update_service_spec.rb'
+- './ee/spec/services/app_sec/dast/sites/find_or_create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_tokens/find_or_create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_validations/find_or_create_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_validations/revoke_service_spec.rb'
+- './ee/spec/services/app_sec/dast/site_validations/runner_service_spec.rb'
+- './ee/spec/services/app_sec/fuzzing/api/ci_configuration_create_service_spec.rb'
+- './ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb'
+- './ee/spec/services/arkose/blocked_users_report_service_spec.rb'
+- './ee/spec/services/arkose/user_verification_service_spec.rb'
+- './ee/spec/services/audit_events/build_service_spec.rb'
+- './ee/spec/services/audit_events/custom_audit_event_service_spec.rb'
+- './ee/spec/services/audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/export_csv_service_spec.rb'
+- './ee/spec/services/audit_events/impersonation_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/protected_branch_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/register_runner_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/release_artifacts_downloaded_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/release_associate_milestone_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/release_created_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/release_updated_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/repository_download_started_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/runner_custom_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/runners_token_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/streaming/headers/base_spec.rb'
+- './ee/spec/services/audit_events/streaming/headers/create_service_spec.rb'
+- './ee/spec/services/audit_events/streaming/headers/destroy_service_spec.rb'
+- './ee/spec/services/audit_events/streaming/headers/update_service_spec.rb'
+- './ee/spec/services/audit_events/unregister_runner_audit_event_service_spec.rb'
+- './ee/spec/services/audit_events/user_impersonation_group_audit_event_service_spec.rb'
+- './ee/spec/services/auto_merge/add_to_merge_train_when_pipeline_succeeds_service_spec.rb'
+- './ee/spec/services/auto_merge/merge_train_service_spec.rb'
+- './ee/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb'
+- './ee/spec/services/award_emojis/add_service_spec.rb'
+- './ee/spec/services/award_emojis/destroy_service_spec.rb'
+- './ee/spec/services/base_count_service_spec.rb'
+- './ee/spec/services/billable_members/destroy_service_spec.rb'
+- './ee/spec/services/boards/create_service_spec.rb'
+- './ee/spec/services/boards/epic_boards/create_service_spec.rb'
+- './ee/spec/services/boards/epic_boards/destroy_service_spec.rb'
+- './ee/spec/services/boards/epic_boards/update_service_spec.rb'
+- './ee/spec/services/boards/epic_boards/visits/create_service_spec.rb'
+- './ee/spec/services/boards/epic_lists/create_service_spec.rb'
+- './ee/spec/services/boards/epic_lists/destroy_service_spec.rb'
+- './ee/spec/services/boards/epic_lists/list_service_spec.rb'
+- './ee/spec/services/boards/epic_lists/update_service_spec.rb'
+- './ee/spec/services/boards/epics/create_service_spec.rb'
+- './ee/spec/services/boards/epics/list_service_spec.rb'
+- './ee/spec/services/boards/epics/move_service_spec.rb'
+- './ee/spec/services/boards/epics/position_create_service_spec.rb'
+- './ee/spec/services/boards/epic_user_preferences/update_service_spec.rb'
+- './ee/spec/services/boards/lists/update_service_spec.rb'
+- './ee/spec/services/boards/update_service_spec.rb'
+- './ee/spec/services/boards/user_preferences/update_service_spec.rb'
+- './ee/spec/services/branches/delete_service_spec.rb'
+- './ee/spec/services/ci/audit_variable_change_service_spec.rb'
+- './ee/spec/services/ci_cd/github_integration_setup_service_spec.rb'
+- './ee/spec/services/ci_cd/github_setup_service_spec.rb'
+- './ee/spec/services/ci_cd/setup_project_spec.rb'
+- './ee/spec/services/ci/compare_license_scanning_reports_collapsed_service_spec.rb'
+- './ee/spec/services/ci/compare_license_scanning_reports_service_spec.rb'
+- './ee/spec/services/ci/compare_metrics_reports_service_spec.rb'
+- './ee/spec/services/ci/compare_security_reports_service_spec.rb'
+- './ee/spec/services/ci/copy_cross_database_associations_service_spec.rb'
+- './ee/spec/services/ci/create_pipeline_service/compliance_spec.rb'
+- './ee/spec/services/ci/create_pipeline_service/cross_needs_artifacts_spec.rb'
+- './ee/spec/services/ci/create_pipeline_service/dast_configuration_spec.rb'
+- './ee/spec/services/ci/create_pipeline_service/needs_spec.rb'
+- './ee/spec/services/ci/create_pipeline_service/runnable_builds_spec.rb'
+- './ee/spec/services/ci/create_pipeline_service_spec.rb'
+- './ee/spec/services/ci/destroy_pipeline_service_spec.rb'
+- './ee/spec/services/ci/external_pull_requests/process_github_event_service_spec.rb'
+- './ee/spec/services/ci/minutes/additional_packs/change_namespace_service_spec.rb'
+- './ee/spec/services/ci/minutes/additional_packs/create_service_spec.rb'
+- './ee/spec/services/ci/minutes/batch_reset_service_spec.rb'
+- './ee/spec/services/ci/minutes/email_notification_service_spec.rb'
+- './ee/spec/services/ci/minutes/refresh_cached_data_service_spec.rb'
+- './ee/spec/services/ci/minutes/reset_usage_service_spec.rb'
+- './ee/spec/services/ci/minutes/track_live_consumption_service_spec.rb'
+- './ee/spec/services/ci/minutes/update_build_minutes_service_spec.rb'
+- './ee/spec/services/ci/minutes/update_project_and_namespace_usage_service_spec.rb'
+- './ee/spec/services/ci/pipeline_bridge_status_service_spec.rb'
+- './ee/spec/services/ci/pipeline_creation/drop_not_runnable_builds_service_spec.rb'
+- './ee/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb'
+- './ee/spec/services/ci/pipeline_trigger_service_spec.rb'
+- './ee/spec/services/ci/play_bridge_service_spec.rb'
+- './ee/spec/services/ci/play_build_service_spec.rb'
+- './ee/spec/services/ci/process_build_service_spec.rb'
+- './ee/spec/services/ci/process_pipeline_service_spec.rb'
+- './ee/spec/services/ci/register_job_service_spec.rb'
+- './ee/spec/services/ci/retry_job_service_spec.rb'
+- './ee/spec/services/ci/retry_pipeline_service_spec.rb'
+- './ee/spec/services/ci/runners/assign_runner_service_spec.rb'
+- './ee/spec/services/ci/runners/register_runner_service_spec.rb'
+- './ee/spec/services/ci/runners/reset_registration_token_service_spec.rb'
+- './ee/spec/services/ci/runners/stale_group_runners_prune_service_spec.rb'
+- './ee/spec/services/ci/runners/unassign_runner_service_spec.rb'
+- './ee/spec/services/ci/runners/unregister_runner_service_spec.rb'
+- './ee/spec/services/ci/subscribe_bridge_service_spec.rb'
+- './ee/spec/services/ci/sync_reports_to_approval_rules_service_spec.rb'
+- './ee/spec/services/ci/trigger_downstream_subscription_service_spec.rb'
+- './ee/spec/services/compliance_management/frameworks/create_service_spec.rb'
+- './ee/spec/services/compliance_management/frameworks/destroy_service_spec.rb'
+- './ee/spec/services/compliance_management/frameworks/update_service_spec.rb'
+- './ee/spec/services/compliance_management/merge_requests/create_compliance_violations_service_spec.rb'
+- './ee/spec/services/concerns/epics/related_epic_links/usage_data_helper_spec.rb'
+- './ee/spec/services/dashboard/environments/list_service_spec.rb'
+- './ee/spec/services/dashboard/operations/list_service_spec.rb'
+- './ee/spec/services/dashboard/projects/create_service_spec.rb'
+- './ee/spec/services/dashboard/projects/list_service_spec.rb'
+- './ee/spec/services/deploy_keys/create_service_spec.rb'
+- './ee/spec/services/deployments/approval_service_spec.rb'
+- './ee/spec/services/deployments/auto_rollback_service_spec.rb'
+- './ee/spec/services/dora/aggregate_metrics_service_spec.rb'
+- './ee/spec/services/ee/alert_management/alerts/update_service_spec.rb'
+- './ee/spec/services/ee/alert_management/create_alert_issue_service_spec.rb'
+- './ee/spec/services/ee/alert_management/http_integrations/create_service_spec.rb'
+- './ee/spec/services/ee/alert_management/http_integrations/update_service_spec.rb'
+- './ee/spec/services/ee/allowed_email_domains/update_service_spec.rb'
+- './ee/spec/services/ee/auth/container_registry_authentication_service_spec.rb'
+- './ee/spec/services/ee/auto_merge_service_spec.rb'
+- './ee/spec/services/ee/boards/issues/create_service_spec.rb'
+- './ee/spec/services/ee/boards/issues/list_service_spec.rb'
+- './ee/spec/services/ee/boards/issues/move_service_spec.rb'
+- './ee/spec/services/ee/boards/lists/create_service_spec.rb'
+- './ee/spec/services/ee/boards/lists/list_service_spec.rb'
+- './ee/spec/services/ee/boards/lists/max_limits_spec.rb'
+- './ee/spec/services/ee/ci/change_variable_service_spec.rb'
+- './ee/spec/services/ee/ci/change_variables_service_spec.rb'
+- './ee/spec/services/ee/ci/job_artifacts/create_service_spec.rb'
+- './ee/spec/services/ee/ci/job_artifacts/destroy_all_expired_service_spec.rb'
+- './ee/spec/services/ee/ci/job_artifacts/destroy_batch_service_spec.rb'
+- './ee/spec/services/ee/ci/pipeline_processing/atomic_processing_service_spec.rb'
+- './ee/spec/services/ee/commits/create_service_spec.rb'
+- './ee/spec/services/ee/deployments/update_environment_service_spec.rb'
+- './ee/spec/services/ee/design_management/delete_designs_service_spec.rb'
+- './ee/spec/services/ee/design_management/save_designs_service_spec.rb'
+- './ee/spec/services/ee/event_create_service_spec.rb'
+- './ee/spec/services/ee/git/branch_push_service_spec.rb'
+- './ee/spec/services/ee/git/wiki_push_service_spec.rb'
+- './ee/spec/services/ee/gpg_keys/create_service_spec.rb'
+- './ee/spec/services/ee/gpg_keys/destroy_service_spec.rb'
+- './ee/spec/services/ee/groups/autocomplete_service_spec.rb'
+- './ee/spec/services/ee/groups/deploy_tokens/create_service_spec.rb'
+- './ee/spec/services/ee/groups/deploy_tokens/destroy_service_spec.rb'
+- './ee/spec/services/ee/groups/deploy_tokens/revoke_service_spec.rb'
+- './ee/spec/services/ee/groups/import_export/export_service_spec.rb'
+- './ee/spec/services/ee/groups/import_export/import_service_spec.rb'
+- './ee/spec/services/ee/incident_management/issuable_escalation_statuses/after_update_service_spec.rb'
+- './ee/spec/services/ee/incident_management/issuable_escalation_statuses/create_service_spec.rb'
+- './ee/spec/services/ee/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb'
+- './ee/spec/services/ee/integrations/test/project_service_spec.rb'
+- './ee/spec/services/ee/ip_restrictions/update_service_spec.rb'
+- './ee/spec/services/ee/issuable/bulk_update_service_spec.rb'
+- './ee/spec/services/ee/issuable/common_system_notes_service_spec.rb'
+- './ee/spec/services/ee/issuable/destroy_service_spec.rb'
+- './ee/spec/services/ee/issue_links/create_service_spec.rb'
+- './ee/spec/services/ee/issues/after_create_service_spec.rb'
+- './ee/spec/services/ee/issues/build_from_vulnerability_service_spec.rb'
+- './ee/spec/services/ee/issues/clone_service_spec.rb'
+- './ee/spec/services/ee/issues/close_service_spec.rb'
+- './ee/spec/services/ee/issues/create_from_vulnerability_data_service_spec.rb'
+- './ee/spec/services/ee/issues/create_service_spec.rb'
+- './ee/spec/services/ee/issues/move_service_spec.rb'
+- './ee/spec/services/ee/issues/reopen_service_spec.rb'
+- './ee/spec/services/ee/issues/update_service_spec.rb'
+- './ee/spec/services/ee/keys/destroy_service_spec.rb'
+- './ee/spec/services/ee/labels/create_service_spec.rb'
+- './ee/spec/services/ee/labels/promote_service_spec.rb'
+- './ee/spec/services/ee/members/create_service_spec.rb'
+- './ee/spec/services/ee/members/destroy_service_spec.rb'
+- './ee/spec/services/ee/members/import_project_team_service_spec.rb'
+- './ee/spec/services/ee/members/invite_service_spec.rb'
+- './ee/spec/services/ee/members/update_service_spec.rb'
+- './ee/spec/services/ee/merge_request_metrics_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/after_create_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/base_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/create_approval_event_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/create_from_vulnerability_data_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/create_pipeline_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/create_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/execute_approval_hooks_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/handle_assignees_change_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/post_merge_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/refresh_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/update_assignees_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/update_reviewers_service_spec.rb'
+- './ee/spec/services/ee/merge_requests/update_service_spec.rb'
+- './ee/spec/services/ee/namespace_settings/update_service_spec.rb'
+- './ee/spec/services/ee/notes/create_service_spec.rb'
+- './ee/spec/services/ee/notes/destroy_service_spec.rb'
+- './ee/spec/services/ee/notes/post_process_service_spec.rb'
+- './ee/spec/services/ee/notes/quick_actions_service_spec.rb'
+- './ee/spec/services/ee/notes/update_service_spec.rb'
+- './ee/spec/services/ee/notification_service_spec.rb'
+- './ee/spec/services/ee/null_notification_service_spec.rb'
+- './ee/spec/services/ee/personal_access_tokens/revoke_service_spec.rb'
+- './ee/spec/services/ee/post_receive_service_spec.rb'
+- './ee/spec/services/ee/preview_markdown_service_spec.rb'
+- './ee/spec/services/ee/projects/autocomplete_service_spec.rb'
+- './ee/spec/services/ee/projects/deploy_tokens/create_service_spec.rb'
+- './ee/spec/services/ee/projects/deploy_tokens/destroy_service_spec.rb'
+- './ee/spec/services/ee/protected_branches/create_service_spec.rb'
+- './ee/spec/services/ee/protected_branches/destroy_service_spec.rb'
+- './ee/spec/services/ee/protected_branches/update_service_spec.rb'
+- './ee/spec/services/ee/quick_actions/target_service_spec.rb'
+- './ee/spec/services/ee/releases/create_evidence_service_spec.rb'
+- './ee/spec/services/ee/resource_events/change_iteration_service_spec.rb'
+- './ee/spec/services/ee/resource_events/change_labels_service_spec.rb'
+- './ee/spec/services/ee/resource_events/merge_into_notes_service_spec.rb'
+- './ee/spec/services/ee/resource_events/synthetic_iteration_notes_builder_service_spec.rb'
+- './ee/spec/services/ee/resource_events/synthetic_weight_notes_builder_service_spec.rb'
+- './ee/spec/services/ee/system_notes/issuables_service_spec.rb'
+- './ee/spec/services/ee/terraform/states/destroy_service_spec.rb'
+- './ee/spec/services/ee/todos/destroy/entity_leave_service_spec.rb'
+- './ee/spec/services/ee/two_factor/destroy_service_spec.rb'
+- './ee/spec/services/ee/users/approve_service_spec.rb'
+- './ee/spec/services/ee/users/authorized_build_service_spec.rb'
+- './ee/spec/services/ee/users/block_service_spec.rb'
+- './ee/spec/services/ee/users/build_service_spec.rb'
+- './ee/spec/services/ee/users/create_service_spec.rb'
+- './ee/spec/services/ee/users/destroy_service_spec.rb'
+- './ee/spec/services/ee/users/migrate_to_ghost_user_service_spec.rb'
+- './ee/spec/services/ee/users/reject_service_spec.rb'
+- './ee/spec/services/ee/users/update_service_spec.rb'
+- './ee/spec/services/ee/vulnerability_feedback_module/update_service_spec.rb'
+- './ee/spec/services/elastic/cluster_reindexing_service_spec.rb'
+- './ee/spec/services/elastic/data_migration_service_spec.rb'
+- './ee/spec/services/elastic/indexing_control_service_spec.rb'
+- './ee/spec/services/elastic/index_projects_by_id_service_spec.rb'
+- './ee/spec/services/elastic/index_projects_by_range_service_spec.rb'
+- './ee/spec/services/elastic/index_projects_service_spec.rb'
+- './ee/spec/services/elastic/metrics_update_service_spec.rb'
+- './ee/spec/services/elastic/process_bookkeeping_service_spec.rb'
+- './ee/spec/services/elastic/process_initial_bookkeeping_service_spec.rb'
+- './ee/spec/services/emails/create_service_spec.rb'
+- './ee/spec/services/emails/destroy_service_spec.rb'
+- './ee/spec/services/epic_issues/create_service_spec.rb'
+- './ee/spec/services/epic_issues/destroy_service_spec.rb'
+- './ee/spec/services/epic_issues/list_service_spec.rb'
+- './ee/spec/services/epic_issues/update_service_spec.rb'
+- './ee/spec/services/epics/close_service_spec.rb'
+- './ee/spec/services/epics/create_service_spec.rb'
+- './ee/spec/services/epics/descendant_count_service_spec.rb'
+- './ee/spec/services/epics/epic_links/create_service_spec.rb'
+- './ee/spec/services/epics/epic_links/destroy_service_spec.rb'
+- './ee/spec/services/epics/epic_links/list_service_spec.rb'
+- './ee/spec/services/epics/epic_links/update_service_spec.rb'
+- './ee/spec/services/epics/issue_promote_service_spec.rb'
+- './ee/spec/services/epics/related_epic_links/create_service_spec.rb'
+- './ee/spec/services/epics/related_epic_links/destroy_service_spec.rb'
+- './ee/spec/services/epics/related_epic_links/list_service_spec.rb'
+- './ee/spec/services/epics/reopen_service_spec.rb'
+- './ee/spec/services/epics/transfer_service_spec.rb'
+- './ee/spec/services/epics/tree_reorder_service_spec.rb'
+- './ee/spec/services/epics/update_dates_service_spec.rb'
+- './ee/spec/services/epics/update_service_spec.rb'
+- './ee/spec/services/external_status_checks/create_service_spec.rb'
+- './ee/spec/services/external_status_checks/destroy_service_spec.rb'
+- './ee/spec/services/external_status_checks/dispatch_service_spec.rb'
+- './ee/spec/services/external_status_checks/update_service_spec.rb'
+- './ee/spec/services/feature_flag_issues/destroy_service_spec.rb'
+- './ee/spec/services/geo/base_file_service_spec.rb'
+- './ee/spec/services/geo/blob_download_service_spec.rb'
+- './ee/spec/services/geo/blob_upload_service_spec.rb'
+- './ee/spec/services/geo/cache_invalidation_event_store_spec.rb'
+- './ee/spec/services/geo/container_repository_sync_service_spec.rb'
+- './ee/spec/services/geo/container_repository_sync_spec.rb'
+- './ee/spec/services/geo/container_repository_updated_event_store_spec.rb'
+- './ee/spec/services/geo/design_repository_sync_service_spec.rb'
+- './ee/spec/services/geo/event_service_spec.rb'
+- './ee/spec/services/geo/file_registry_removal_service_spec.rb'
+- './ee/spec/services/geo/files_expire_service_spec.rb'
+- './ee/spec/services/geo/framework_repository_sync_service_spec.rb'
+- './ee/spec/services/geo/graphql_request_service_spec.rb'
+- './ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb'
+- './ee/spec/services/geo/hashed_storage_attachments_migration_service_spec.rb'
+- './ee/spec/services/geo/hashed_storage_migrated_event_store_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/node_create_service_spec.rb'
+- './ee/spec/services/geo/node_status_request_service_spec.rb'
+- './ee/spec/services/geo/node_update_service_spec.rb'
+- './ee/spec/services/geo/project_housekeeping_service_spec.rb'
+- './ee/spec/services/geo/prune_event_log_service_spec.rb'
+- './ee/spec/services/geo/registry_consistency_service_spec.rb'
+- './ee/spec/services/geo/rename_repository_service_spec.rb'
+- './ee/spec/services/geo/replication_toggle_request_service_spec.rb'
+- './ee/spec/services/geo/repositories_changed_event_store_spec.rb'
+- './ee/spec/services/geo/repository_base_sync_service_spec.rb'
+- './ee/spec/services/geo/repository_created_event_store_spec.rb'
+- './ee/spec/services/geo/repository_deleted_event_store_spec.rb'
+- './ee/spec/services/geo/repository_destroy_service_spec.rb'
+- './ee/spec/services/geo/repository_registry_removal_service_spec.rb'
+- './ee/spec/services/geo/repository_renamed_event_store_spec.rb'
+- './ee/spec/services/geo/repository_sync_service_spec.rb'
+- './ee/spec/services/geo/repository_updated_event_store_spec.rb'
+- './ee/spec/services/geo/repository_updated_service_spec.rb'
+- './ee/spec/services/geo/repository_verification_primary_service_spec.rb'
+- './ee/spec/services/geo/repository_verification_reset_spec.rb'
+- './ee/spec/services/geo/repository_verification_secondary_service_spec.rb'
+- './ee/spec/services/geo/reset_checksum_event_store_spec.rb'
+- './ee/spec/services/geo/wiki_sync_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/activate_awaiting_users_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/activate_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/apply_trial_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/create_hand_raise_lead_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/create_trial_or_lead_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/extend_reactivate_trial_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/fetch_purchase_eligible_namespaces_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/fetch_subscription_plans_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/notify_seats_exceeded_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/plan_upgrade_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/preview_billable_user_change_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/reconciliations/calculate_seat_count_data_service_spec.rb'
+- './ee/spec/services/gitlab_subscriptions/reconciliations/check_seat_usage_alerts_eligibility_service_spec.rb'
+- './ee/spec/services/group_saml/group_managed_accounts/clean_up_members_service_spec.rb'
+- './ee/spec/services/group_saml/group_managed_accounts/transfer_membership_service_spec.rb'
+- './ee/spec/services/group_saml/identity/destroy_service_spec.rb'
+- './ee/spec/services/group_saml/saml_group_links/create_service_spec.rb'
+- './ee/spec/services/group_saml/saml_group_links/destroy_service_spec.rb'
+- './ee/spec/services/group_saml/saml_provider/create_service_spec.rb'
+- './ee/spec/services/group_saml/saml_provider/update_service_spec.rb'
+- './ee/spec/services/group_saml/sign_up_service_spec.rb'
+- './ee/spec/services/groups/create_service_spec.rb'
+- './ee/spec/services/groups/destroy_service_spec.rb'
+- './ee/spec/services/groups/epics_count_service_spec.rb'
+- './ee/spec/services/groups/mark_for_deletion_service_spec.rb'
+- './ee/spec/services/groups/memberships/export_service_spec.rb'
+- './ee/spec/services/groups/participants_service_spec.rb'
+- './ee/spec/services/groups/restore_service_spec.rb'
+- './ee/spec/services/groups/schedule_bulk_repository_shard_moves_service_spec.rb'
+- './ee/spec/services/groups/seat_usage_export_service_spec.rb'
+- './ee/spec/services/groups/sync_service_spec.rb'
+- './ee/spec/services/groups/transfer_service_spec.rb'
+- './ee/spec/services/groups/update_repository_storage_service_spec.rb'
+- './ee/spec/services/groups/update_service_spec.rb'
+- './ee/spec/services/historical_user_data/csv_service_spec.rb'
+- './ee/spec/services/ide/schemas_config_service_spec.rb'
+- './ee/spec/services/incident_management/create_incident_sla_exceeded_label_service_spec.rb'
+- './ee/spec/services/incident_management/escalation_policies/create_service_spec.rb'
+- './ee/spec/services/incident_management/escalation_policies/destroy_service_spec.rb'
+- './ee/spec/services/incident_management/escalation_policies/update_service_spec.rb'
+- './ee/spec/services/incident_management/escalation_rules/destroy_service_spec.rb'
+- './ee/spec/services/incident_management/incidents/create_sla_service_spec.rb'
+- './ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb'
+- './ee/spec/services/incident_management/issuable_resource_links/create_service_spec.rb'
+- './ee/spec/services/incident_management/issuable_resource_links/destroy_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_rotations/create_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_rotations/destroy_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_rotations/remove_participant_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_rotations/remove_participants_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_schedules/create_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_schedules/destroy_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_schedules/update_service_spec.rb'
+- './ee/spec/services/incident_management/oncall_shifts/read_service_spec.rb'
+- './ee/spec/services/incident_management/pending_escalations/create_service_spec.rb'
+- './ee/spec/services/incident_management/pending_escalations/process_service_spec.rb'
+- './ee/spec/services/issuable/destroy_label_links_service_spec.rb'
+- './ee/spec/services/issue_feature_flags/list_service_spec.rb'
+- './ee/spec/services/issues/build_service_spec.rb'
+- './ee/spec/services/issues/duplicate_service_spec.rb'
+- './ee/spec/services/issues/export_csv_service_spec.rb'
+- './ee/spec/services/iterations/cadences/create_iterations_in_advance_service_spec.rb'
+- './ee/spec/services/iterations/cadences/create_service_spec.rb'
+- './ee/spec/services/iterations/cadences/destroy_service_spec.rb'
+- './ee/spec/services/iterations/cadences/update_service_spec.rb'
+- './ee/spec/services/iterations/create_service_spec.rb'
+- './ee/spec/services/iterations/delete_service_spec.rb'
+- './ee/spec/services/iterations/roll_over_issues_service_spec.rb'
+- './ee/spec/services/iterations/update_service_spec.rb'
+- './ee/spec/services/jira/jql_builder_service_spec.rb'
+- './ee/spec/services/jira/requests/issues/list_service_spec.rb'
+- './ee/spec/services/keys/create_service_spec.rb'
+- './ee/spec/services/keys/last_used_service_spec.rb'
+- './ee/spec/services/ldap_group_reset_service_spec.rb'
+- './ee/spec/services/lfs/lock_file_service_spec.rb'
+- './ee/spec/services/lfs/unlock_file_service_spec.rb'
+- './ee/spec/services/licenses/destroy_service_spec.rb'
+- './ee/spec/services/members/activate_service_spec.rb'
+- './ee/spec/services/members/await_service_spec.rb'
+- './ee/spec/services/merge_commits/export_csv_service_spec.rb'
+- './ee/spec/services/merge_request_approval_settings/update_service_spec.rb'
+- './ee/spec/services/merge_requests/approval_service_spec.rb'
+- './ee/spec/services/merge_requests/build_service_spec.rb'
+- './ee/spec/services/merge_requests/mergeability/check_approved_service_spec.rb'
+- './ee/spec/services/merge_requests/mergeability/check_blocked_by_other_mrs_service_spec.rb'
+- './ee/spec/services/merge_requests/mergeability/check_denied_policies_service_spec.rb'
+- './ee/spec/services/merge_requests/merge_service_spec.rb'
+- './ee/spec/services/merge_requests/merge_to_ref_service_spec.rb'
+- './ee/spec/services/merge_requests/push_options_handler_service_spec.rb'
+- './ee/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb'
+- './ee/spec/services/merge_requests/remove_approval_service_spec.rb'
+- './ee/spec/services/merge_requests/reset_approvals_service_spec.rb'
+- './ee/spec/services/merge_requests/stream_approval_audit_event_service_spec.rb'
+- './ee/spec/services/merge_requests/sync_code_owner_approval_rules_spec.rb'
+- './ee/spec/services/merge_requests/sync_report_approver_approval_rules_spec.rb'
+- './ee/spec/services/merge_requests/update_blocks_service_spec.rb'
+- './ee/spec/services/merge_trains/check_status_service_spec.rb'
+- './ee/spec/services/merge_trains/create_pipeline_service_spec.rb'
+- './ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb'
+- './ee/spec/services/merge_trains/refresh_service_spec.rb'
+- './ee/spec/services/milestones/destroy_service_spec.rb'
+- './ee/spec/services/milestones/promote_service_spec.rb'
+- './ee/spec/services/milestones/update_service_spec.rb'
+- './ee/spec/services/namespaces/free_user_cap/deactivate_members_over_limit_service_spec.rb'
+- './ee/spec/services/namespaces/free_user_cap/remove_group_group_links_outside_hierarchy_service_spec.rb'
+- './ee/spec/services/namespaces/free_user_cap/remove_project_group_links_outside_hierarchy_service_spec.rb'
+- './ee/spec/services/namespaces/free_user_cap/update_prevent_sharing_outside_hierarchy_service_spec.rb'
+- './ee/spec/services/namespaces/in_product_marketing_emails_service_spec.rb'
+- './ee/spec/services/namespaces/storage/email_notification_service_spec.rb'
+- './ee/spec/services/path_locks/lock_service_spec.rb'
+- './ee/spec/services/path_locks/unlock_service_spec.rb'
+- './ee/spec/services/personal_access_tokens/create_service_audit_log_spec.rb'
+- './ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb'
+- './ee/spec/services/personal_access_tokens/instance/update_lifetime_service_spec.rb'
+- './ee/spec/services/personal_access_tokens/revoke_invalid_tokens_spec.rb'
+- './ee/spec/services/personal_access_tokens/revoke_service_audit_log_spec.rb'
+- './ee/spec/services/personal_access_tokens/rotation_verifier_service_spec.rb'
+- './ee/spec/services/projects/after_rename_service_spec.rb'
+- './ee/spec/services/projects/alerting/notify_service_spec.rb'
+- './ee/spec/services/projects/cleanup_service_spec.rb'
+- './ee/spec/services/projects/create_from_template_service_spec.rb'
+- './ee/spec/services/projects/create_service_spec.rb'
+- './ee/spec/services/projects/destroy_service_spec.rb'
+- './ee/spec/services/projects/disable_deploy_key_service_spec.rb'
+- './ee/spec/services/projects/disable_legacy_inactive_projects_service_spec.rb'
+- './ee/spec/services/projects/enable_deploy_key_service_spec.rb'
+- './ee/spec/services/projects/fork_service_spec.rb'
+- './ee/spec/services/projects/gitlab_projects_import_service_spec.rb'
+- './ee/spec/services/projects/group_links/create_service_spec.rb'
+- './ee/spec/services/projects/group_links/destroy_service_spec.rb'
+- './ee/spec/services/projects/group_links/update_service_spec.rb'
+- './ee/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
+- './ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
+- './ee/spec/services/projects/import_export/export_service_spec.rb'
+- './ee/spec/services/projects/import_service_spec.rb'
+- './ee/spec/services/projects/licenses/create_policy_service_spec.rb'
+- './ee/spec/services/projects/licenses/update_policy_service_spec.rb'
+- './ee/spec/services/projects/mark_for_deletion_service_spec.rb'
+- './ee/spec/services/projects/open_issues_count_service_spec.rb'
+- './ee/spec/services/projects/operations/update_service_spec.rb'
+- './ee/spec/services/projects/prometheus/alerts/notify_service_spec.rb'
+- './ee/spec/services/projects/protect_default_branch_service_spec.rb'
+- './ee/spec/services/projects/restore_service_spec.rb'
+- './ee/spec/services/projects/setup_ci_cd_spec.rb'
+- './ee/spec/services/projects/slack_application_install_service_spec.rb'
+- './ee/spec/services/projects/transfer_service_spec.rb'
+- './ee/spec/services/projects/update_mirror_service_spec.rb'
+- './ee/spec/services/projects/update_service_spec.rb'
+- './ee/spec/services/protected_environments/base_service_spec.rb'
+- './ee/spec/services/protected_environments/create_service_spec.rb'
+- './ee/spec/services/protected_environments/destroy_service_spec.rb'
+- './ee/spec/services/protected_environments/environment_dropdown_service_spec.rb'
+- './ee/spec/services/protected_environments/search_service_spec.rb'
+- './ee/spec/services/protected_environments/update_service_spec.rb'
+- './ee/spec/services/push_rules/create_or_update_service_spec.rb'
+- './ee/spec/services/quality_management/test_cases/create_service_spec.rb'
+- './ee/spec/services/quick_actions/interpret_service_spec.rb'
+- './ee/spec/services/releases/create_service_spec.rb'
+- './ee/spec/services/releases/update_service_spec.rb'
+- './ee/spec/services/repositories/housekeeping_service_spec.rb'
+- './ee/spec/services/requirements_management/export_csv_service_spec.rb'
+- './ee/spec/services/requirements_management/import_csv_service_spec.rb'
+- './ee/spec/services/requirements_management/map_export_fields_service_spec.rb'
+- './ee/spec/services/requirements_management/prepare_import_csv_service_spec.rb'
+- './ee/spec/services/requirements_management/process_test_reports_service_spec.rb'
+- './ee/spec/services/requirements_management/update_requirement_service_spec.rb'
+- './ee/spec/services/resource_access_tokens/create_service_spec.rb'
+- './ee/spec/services/resource_access_tokens/revoke_service_spec.rb'
+- './ee/spec/services/resource_events/change_weight_service_spec.rb'
+- './ee/spec/services/search/global_service_spec.rb'
+- './ee/spec/services/search/group_service_spec.rb'
+- './ee/spec/services/search/project_service_spec.rb'
+- './ee/spec/services/search_service_spec.rb'
+- './ee/spec/services/search/snippet_service_spec.rb'
+- './ee/spec/services/security/auto_fix_label_service_spec.rb'
+- './ee/spec/services/security/auto_fix_service_spec.rb'
+- './ee/spec/services/security/configuration/save_auto_fix_service_spec.rb'
+- './ee/spec/services/security/dependency_list_service_spec.rb'
+- './ee/spec/services/security/findings/cleanup_service_spec.rb'
+- './ee/spec/services/security/ingestion/finding_map_collection_spec.rb'
+- './ee/spec/services/security/ingestion/finding_map_spec.rb'
+- './ee/spec/services/security/ingestion/ingest_report_service_spec.rb'
+- './ee/spec/services/security/ingestion/ingest_report_slice_service_spec.rb'
+- './ee/spec/services/security/ingestion/ingest_reports_service_spec.rb'
+- './ee/spec/services/security/ingestion/mark_as_resolved_service_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/attach_findings_to_vulnerabilities_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/hooks_execution_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_finding_evidence_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_finding_identifiers_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_finding_links_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_finding_pipelines_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_finding_signatures_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_findings_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_identifiers_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_issue_links_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_remediations_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/create_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/mark_resolved_as_detected_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_vulnerability_flags_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/ingest_vulnerability_statistics_spec.rb'
+- './ee/spec/services/security/ingestion/tasks/update_vulnerability_uuids_spec.rb'
+- './ee/spec/services/security/merge_reports_service_spec.rb'
+- './ee/spec/services/security/orchestration/assign_service_spec.rb'
+- './ee/spec/services/security/orchestration/unassign_service_spec.rb'
+- './ee/spec/services/security/override_uuids_service_spec.rb'
+- './ee/spec/services/security/report_fetch_service_spec.rb'
+- './ee/spec/services/security/report_summary_service_spec.rb'
+- './ee/spec/services/security/scanned_resources_counting_service_spec.rb'
+- './ee/spec/services/security/scanned_resources_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/ci_configuration_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/create_pipeline_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/fetch_policy_approvers_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/fetch_policy_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/on_demand_scan_pipeline_configuration_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/operational_vulnerabilities_configuration_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/policy_commit_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/policy_configuration_validation_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/process_policy_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/process_rule_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/process_scan_result_policy_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/project_create_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/rule_schedule_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/scan_pipeline_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/sync_opened_merge_requests_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/sync_open_merge_requests_head_pipeline_service_spec.rb'
+- './ee/spec/services/security/security_orchestration_policies/validate_policy_service_spec.rb'
+- './ee/spec/services/security/store_findings_metadata_service_spec.rb'
+- './ee/spec/services/security/store_grouped_scans_service_spec.rb'
+- './ee/spec/services/security/store_scan_service_spec.rb'
+- './ee/spec/services/security/store_scans_service_spec.rb'
+- './ee/spec/services/security/token_revocation_service_spec.rb'
+- './ee/spec/services/security/track_scan_service_spec.rb'
+- './ee/spec/services/security/update_training_service_spec.rb'
+- './ee/spec/services/security/vulnerability_counting_service_spec.rb'
+- './ee/spec/services/sitemap/create_service_spec.rb'
+- './ee/spec/services/slash_commands/global_slack_handler_spec.rb'
+- './ee/spec/services/software_license_policies/create_service_spec.rb'
+- './ee/spec/services/software_license_policies/update_service_spec.rb'
+- './ee/spec/services/start_pull_mirroring_service_spec.rb'
+- './ee/spec/services/status_page/mark_for_publication_service_spec.rb'
+- './ee/spec/services/status_page/publish_attachments_service_spec.rb'
+- './ee/spec/services/status_page/publish_details_service_spec.rb'
+- './ee/spec/services/status_page/publish_list_service_spec.rb'
+- './ee/spec/services/status_page/publish_service_spec.rb'
+- './ee/spec/services/status_page/trigger_publish_service_spec.rb'
+- './ee/spec/services/status_page/unpublish_details_service_spec.rb'
+- './ee/spec/services/system_notes/epics_service_spec.rb'
+- './ee/spec/services/system_note_service_spec.rb'
+- './ee/spec/services/system_notes/escalations_service_spec.rb'
+- './ee/spec/services/system_notes/merge_train_service_spec.rb'
+- './ee/spec/services/system_notes/vulnerabilities_service_spec.rb'
+- './ee/spec/services/timebox_report_service_spec.rb'
+- './ee/spec/services/timelogs/create_service_spec.rb'
+- './ee/spec/services/todos/allowed_target_filter_service_spec.rb'
+- './ee/spec/services/todos/destroy/confidential_epic_service_spec.rb'
+- './ee/spec/services/todo_service_spec.rb'
+- './ee/spec/services/upcoming_reconciliations/update_service_spec.rb'
+- './ee/spec/services/user_permissions/export_service_spec.rb'
+- './ee/spec/services/users/abuse/excessive_projects_download_ban_service_spec.rb'
+- './ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb'
+- './ee/spec/services/users/abuse/namespace_bans/create_service_spec.rb'
+- './ee/spec/services/users/abuse/namespace_bans/destroy_service_spec.rb'
+- './ee/spec/services/users/captcha_challenge_service_spec.rb'
+- './ee/spec/services/users_ops_dashboard_projects/destroy_service_spec.rb'
+- './ee/spec/services/users/update_highest_member_role_service_spec.rb'
+- './ee/spec/services/vulnerabilities/confirm_service_spec.rb'
+- './ee/spec/services/vulnerabilities/create_from_security_finding_service_spec.rb'
+- './ee/spec/services/vulnerabilities/create_service_spec.rb'
+- './ee/spec/services/vulnerabilities/destroy_dismissal_feedback_service_spec.rb'
+- './ee/spec/services/vulnerabilities/dismiss_service_spec.rb'
+- './ee/spec/services/vulnerabilities/finding_dismiss_service_spec.rb'
+- './ee/spec/services/vulnerabilities/findings/create_from_security_finding_service_spec.rb'
+- './ee/spec/services/vulnerabilities/historical_statistics/adjustment_service_spec.rb'
+- './ee/spec/services/vulnerabilities/historical_statistics/deletion_service_spec.rb'
+- './ee/spec/services/vulnerabilities/manually_create_service_spec.rb'
+- './ee/spec/services/vulnerabilities/resolve_service_spec.rb'
+- './ee/spec/services/vulnerabilities/revert_to_detected_service_spec.rb'
+- './ee/spec/services/vulnerabilities/security_finding/create_issue_service_spec.rb'
+- './ee/spec/services/vulnerabilities/starboard_vulnerability_create_service_spec.rb'
+- './ee/spec/services/vulnerabilities/starboard_vulnerability_resolve_service_spec.rb'
+- './ee/spec/services/vulnerabilities/statistics/adjustment_service_spec.rb'
+- './ee/spec/services/vulnerabilities/statistics/update_service_spec.rb'
+- './ee/spec/services/vulnerabilities/update_service_spec.rb'
+- './ee/spec/services/vulnerabilities/user_notes_count_service_spec.rb'
+- './ee/spec/services/vulnerability_exports/create_service_spec.rb'
+- './ee/spec/services/vulnerability_exports/exporters/csv_service_spec.rb'
+- './ee/spec/services/vulnerability_exports/export_service_spec.rb'
+- './ee/spec/services/vulnerability_external_issue_links/create_service_spec.rb'
+- './ee/spec/services/vulnerability_external_issue_links/destroy_service_spec.rb'
+- './ee/spec/services/vulnerability_feedback/create_service_spec.rb'
+- './ee/spec/services/vulnerability_feedback/destroy_service_spec.rb'
+- './ee/spec/services/vulnerability_issue_links/create_service_spec.rb'
+- './ee/spec/services/vulnerability_issue_links/delete_service_spec.rb'
+- './ee/spec/services/vulnerability_scanners/list_service_spec.rb'
+- './ee/spec/services/web_hook_service_spec.rb'
+- './ee/spec/services/wiki_pages/create_service_spec.rb'
+- './ee/spec/services/wiki_pages/destroy_service_spec.rb'
+- './ee/spec/services/wiki_pages/update_service_spec.rb'
+- './ee/spec/services/wikis/create_attachment_service_spec.rb'
+- './ee/spec/services/work_items/update_service_spec.rb'
+- './ee/spec/services/work_items/widgets/weight_service/update_service_spec.rb'
+- './ee/spec/tasks/geo/git_rake_spec.rb'
+- './ee/spec/tasks/geo_rake_spec.rb'
+- './ee/spec/tasks/gitlab/check_rake_spec.rb'
+- './ee/spec/tasks/gitlab/elastic_rake_spec.rb'
+- './ee/spec/tasks/gitlab/geo_rake_spec.rb'
+- './ee/spec/tasks/gitlab/license_rake_spec.rb'
+- './ee/spec/tasks/gitlab/seed/group_seed_rake_spec.rb'
+- './ee/spec/tasks/gitlab/spdx_rake_spec.rb'
+- './ee/spec/tasks/gitlab/uploads/migrate_rake_spec.rb'
+- './ee/spec/validators/json_schema_validator_spec.rb'
+- './ee/spec/validators/ldap_filter_validator_spec.rb'
+- './ee/spec/validators/password/complexity_validator_spec.rb'
+- './ee/spec/validators/user_existence_validator_spec.rb'
+- './ee/spec/views/admin/application_settings/_deletion_protection_settings.html.haml_spec.rb'
+- './ee/spec/views/admin/application_settings/_elasticsearch_form.html.haml_spec.rb'
+- './ee/spec/views/admin/application_settings/general.html.haml_spec.rb'
+- './ee/spec/views/admin/application_settings/_git_abuse_rate_limit.html.haml_spec.rb'
+- './ee/spec/views/admin/dashboard/index.html.haml_spec.rb'
+- './ee/spec/views/admin/dev_ops_report/show.html.haml_spec.rb'
+- './ee/spec/views/admin/groups/_form.html.haml_spec.rb'
+- './ee/spec/views/admin/identities/index.html.haml_spec.rb'
+- './ee/spec/views/admin/push_rules/_merge_request_approvals.html.haml_spec.rb'
+- './ee/spec/views/admin/users/_credit_card_info.html.haml_spec.rb'
+- './ee/spec/views/admin/users/index.html.haml_spec.rb'
+- './ee/spec/views/admin/users/show.html.haml_spec.rb'
+- './ee/spec/views/clusters/clusters/show.html.haml_spec.rb'
+- './ee/spec/views/compliance_management/compliance_framework/_compliance_framework_badge.html.haml_spec.rb'
+- './ee/spec/views/compliance_management/compliance_framework/_project_settings.html.haml_spec.rb'
+- './ee/spec/views/devise/sessions/new.html.haml_spec.rb'
+- './ee/spec/views/groups/billings/index.html.haml_spec.rb'
+- './ee/spec/views/groups/compliance_frameworks/edit.html.haml_spec.rb'
+- './ee/spec/views/groups/_compliance_frameworks.html.haml_spec.rb'
+- './ee/spec/views/groups/compliance_frameworks/new.html.haml_spec.rb'
+- './ee/spec/views/groups/edit.html.haml_spec.rb'
+- './ee/spec/views/groups/feature_discovery_moments/advanced_features_dashboard.html.haml_spec.rb'
+- './ee/spec/views/groups/group_members/index.html.haml_spec.rb'
+- './ee/spec/views/groups/hook_logs/show.html.haml_spec.rb'
+- './ee/spec/views/groups/hooks/edit.html.haml_spec.rb'
+- './ee/spec/views/groups/security/discover/show.html.haml_spec.rb'
+- './ee/spec/views/groups/settings/_remove.html.haml_spec.rb'
+- './ee/spec/views/groups/settings/reporting/show.html.haml_spec.rb'
+- './ee/spec/views/groups/show.html.haml_spec.rb'
+- './ee/spec/views/groups/usage_quotas/index.html.haml_spec.rb'
+- './ee/spec/views/layouts/application.html.haml_spec.rb'
+- './ee/spec/views/layouts/checkout.html.haml_spec.rb'
+- './ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb'
+- './ee/spec/views/layouts/header/_ee_subscribable_banner.html.haml_spec.rb'
+- './ee/spec/views/layouts/header/help_dropdown/_cross_stage_fdm.html.haml_spec.rb'
+- './ee/spec/views/layouts/header/_new_dropdown.haml_spec.rb'
+- './ee/spec/views/layouts/header/_read_only_banner.html.haml_spec.rb'
+- './ee/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb'
+- './ee/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb'
+- './ee/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb'
+- './ee/spec/views/layouts/nav/sidebar/_push_rules_link.html.haml_spec.rb'
+- './ee/spec/views/layouts/_search.html.haml_spec.rb'
+- './ee/spec/views/operations/environments.html.haml_spec.rb'
+- './ee/spec/views/operations/index.html.haml_spec.rb'
+- './ee/spec/views/profiles/preferences/show.html.haml_spec.rb'
+- './ee/spec/views/projects/edit.html.haml_spec.rb'
+- './ee/spec/views/projects/empty.html.haml_spec.rb'
+- './ee/spec/views/projects/issues/show.html.haml_spec.rb'
+- './ee/spec/views/projects/merge_requests/_merge_request_approvals.html.haml_spec.rb'
+- './ee/spec/views/projects/_merge_request_status_checks_settings.html.haml_spec.rb'
+- './ee/spec/views/projects/on_demand_scans/index.html.haml_spec.rb'
+- './ee/spec/views/projects/pipelines/_tabs_content.html.haml_spec.rb'
+- './ee/spec/views/projects/project_members/index.html.haml_spec.rb'
+- './ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb'
+- './ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
+- './ee/spec/views/projects/security/dast_scanner_profiles/edit.html.haml_spec.rb'
+- './ee/spec/views/projects/security/dast_scanner_profiles/new.html.haml_spec.rb'
+- './ee/spec/views/projects/security/dast_site_profiles/edit.html.haml_spec.rb'
+- './ee/spec/views/projects/security/dast_site_profiles/new.html.haml_spec.rb'
+- './ee/spec/views/projects/security/discover/show.html.haml_spec.rb'
+- './ee/spec/views/projects/security/policies/index.html.haml_spec.rb'
+- './ee/spec/views/projects/security/sast_configuration/show.html.haml_spec.rb'
+- './ee/spec/views/projects/settings/subscriptions/_index.html.haml_spec.rb'
+- './ee/spec/views/projects/show.html.haml_spec.rb'
+- './ee/spec/views/registrations/groups/new.html.haml_spec.rb'
+- './ee/spec/views/registrations/groups_projects/new.html.haml_spec.rb'
+- './ee/spec/views/registrations/projects/new.html.haml_spec.rb'
+- './ee/spec/views/registrations/welcome/continuous_onboarding_getting_started.html.haml_spec.rb'
+- './ee/spec/views/registrations/welcome/show.html.haml_spec.rb'
+- './ee/spec/views/search/_category.html.haml_spec.rb'
+- './ee/spec/views/shared/access_tokens/_table.html.haml_spec.rb'
+- './ee/spec/views/shared/billings/_billing_plan_actions.html.haml_spec.rb'
+- './ee/spec/views/shared/billings/_billing_plan.html.haml_spec.rb'
+- './ee/spec/views/shared/billings/_billing_plans.html.haml_spec.rb'
+- './ee/spec/views/shared/billings/_eoa_bronze_plan_banner.html.haml_spec.rb'
+- './ee/spec/views/shared/billings/_trial_status.html.haml_spec.rb'
+- './ee/spec/views/shared/_clone_panel.html.haml_spec.rb'
+- './ee/spec/views/shared/credentials_inventory/_expiry_date.html.haml_spec.rb'
+- './ee/spec/views/shared/credentials_inventory/gpg_keys/_gpg_key.html.haml_spec.rb'
+- './ee/spec/views/shared/credentials_inventory/personal_access_tokens/_personal_access_token.html.haml_spec.rb'
+- './ee/spec/views/shared/credentials_inventory/project_access_tokens/_project_access_token.html.haml_spec.rb'
+- './ee/spec/views/shared/credentials_inventory/ssh_keys/_ssh_key.html.haml_spec.rb'
+- './ee/spec/views/shared/issuable/_approver_suggestion.html.haml_spec.rb'
+- './ee/spec/views/shared/issuable/_epic_dropdown.html.haml_spec.rb'
+- './ee/spec/views/shared/issuable/_health_status_dropdown.html.haml_spec.rb'
+- './ee/spec/views/shared/issuable/_iterations_dropdown.html.haml_spec.rb'
+- './ee/spec/views/shared/issuable/_sidebar.html.haml_spec.rb'
+- './ee/spec/views/shared/_kerberos_clone_button.html.haml_spec.rb'
+- './ee/spec/views/shared/labels/_create_label_help_text.html.haml_spec.rb'
+- './ee/spec/views/shared/milestones/_milestone.html.haml_spec.rb'
+- './ee/spec/views/shared/_mirror_status.html.haml_spec.rb'
+- './ee/spec/views/shared/_mirror_update_button.html.haml_spec.rb'
+- './ee/spec/views/shared/_namespace_user_cap_reached_alert.html.haml_spec.rb'
+- './ee/spec/views/shared/promotions/_promotion_link_project.html.haml_spec.rb'
+- './ee/spec/views/subscriptions/buy_minutes.html.haml_spec.rb'
+- './ee/spec/views/subscriptions/buy_storage.html.haml_spec.rb'
+- './ee/spec/views/subscriptions/groups/edit.html.haml_spec.rb'
+- './ee/spec/views/subscriptions/new.html.haml_spec.rb'
+- './ee/spec/views/trial_registrations/new.html.haml_spec.rb'
+- './ee/spec/views/trials/_skip_trial.html.haml_spec.rb'
+- './ee/spec/workers/active_user_count_threshold_worker_spec.rb'
+- './ee/spec/workers/adjourned_group_deletion_worker_spec.rb'
+- './ee/spec/workers/adjourned_project_deletion_worker_spec.rb'
+- './ee/spec/workers/adjourned_projects_deletion_cron_worker_spec.rb'
+- './ee/spec/workers/admin_emails_worker_spec.rb'
+- './ee/spec/workers/analytics/code_review_metrics_worker_spec.rb'
+- './ee/spec/workers/analytics/cycle_analytics/consistency_worker_spec.rb'
+- './ee/spec/workers/analytics/cycle_analytics/incremental_worker_spec.rb'
+- './ee/spec/workers/analytics/cycle_analytics/reaggregation_worker_spec.rb'
+- './ee/spec/workers/analytics/devops_adoption/create_all_snapshots_worker_spec.rb'
+- './ee/spec/workers/analytics/devops_adoption/create_snapshot_worker_spec.rb'
+- './ee/spec/workers/approval_rules/external_approval_rule_payload_worker_spec.rb'
+- './ee/spec/workers/app_sec/dast/profile_schedule_worker_spec.rb'
+- './ee/spec/workers/app_sec/dast/scanner_profiles_builds/consistency_worker_spec.rb'
+- './ee/spec/workers/app_sec/dast/scans/consistency_worker_spec.rb'
+- './ee/spec/workers/app_sec/dast/site_profiles_builds/consistency_worker_spec.rb'
+- './ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb'
+- './ee/spec/workers/audit_events/user_impersonation_event_create_worker_spec.rb'
+- './ee/spec/workers/auth/saml_group_sync_worker_spec.rb'
+- './ee/spec/workers/ci/batch_reset_minutes_worker_spec.rb'
+- './ee/spec/workers/ci/initial_pipeline_process_worker_spec.rb'
+- './ee/spec/workers/ci/minutes/refresh_cached_data_worker_spec.rb'
+- './ee/spec/workers/ci/minutes/update_project_and_namespace_usage_worker_spec.rb'
+- './ee/spec/workers/ci/runners/stale_group_runners_prune_cron_worker_spec.rb'
+- './ee/spec/workers/ci/sync_reports_to_report_approval_rules_worker_spec.rb'
+- './ee/spec/workers/ci/trigger_downstream_subscriptions_worker_spec.rb'
+- './ee/spec/workers/ci/upstream_projects_subscriptions_cleanup_worker_spec.rb'
+- './ee/spec/workers/clear_shared_runners_minutes_worker_spec.rb'
+- './ee/spec/workers/compliance_management/chain_of_custody_report_worker_spec.rb'
+- './ee/spec/workers/compliance_management/merge_requests/compliance_violations_worker_spec.rb'
+- './ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
+- './ee/spec/workers/concerns/elastic/migration_obsolete_spec.rb'
+- './ee/spec/workers/concerns/elastic/migration_options_spec.rb'
+- './ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb'
+- './ee/spec/workers/create_github_webhook_worker_spec.rb'
+- './ee/spec/workers/deployments/auto_rollback_worker_spec.rb'
+- './ee/spec/workers/dora/daily_metrics/refresh_worker_spec.rb'
+- './ee/spec/workers/ee/arkose/blocked_users_report_worker_spec.rb'
+- './ee/spec/workers/ee/ci/build_finished_worker_spec.rb'
+- './ee/spec/workers/ee/issuable_export_csv_worker_spec.rb'
+- './ee/spec/workers/ee/namespaces/in_product_marketing_emails_worker_spec.rb'
+- './ee/spec/workers/ee/namespaces/root_statistics_worker_spec.rb'
+- './ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb'
+- './ee/spec/workers/ee/repository_check/batch_worker_spec.rb'
+- './ee/spec/workers/ee/repository_check/single_repository_worker_spec.rb'
+- './ee/spec/workers/elastic_association_indexer_worker_spec.rb'
+- './ee/spec/workers/elastic_cluster_reindexing_cron_worker_spec.rb'
+- './ee/spec/workers/elastic_commit_indexer_worker_spec.rb'
+- './ee/spec/workers/elastic_delete_project_worker_spec.rb'
+- './ee/spec/workers/elastic_full_index_worker_spec.rb'
+- './ee/spec/workers/elastic_index_bulk_cron_worker_spec.rb'
+- './ee/spec/workers/elastic_indexing_control_worker_spec.rb'
+- './ee/spec/workers/elastic_index_initial_bulk_cron_worker_spec.rb'
+- './ee/spec/workers/elastic/migration_worker_spec.rb'
+- './ee/spec/workers/elastic_namespace_indexer_worker_spec.rb'
+- './ee/spec/workers/elastic_namespace_rollout_worker_spec.rb'
+- './ee/spec/workers/elastic/project_transfer_worker_spec.rb'
+- './ee/spec/workers/elastic_remove_expired_namespace_subscriptions_from_index_cron_worker_spec.rb'
+- './ee/spec/workers/epics/new_epic_issue_worker_spec.rb'
+- './ee/spec/workers/geo/batch_event_create_worker_spec.rb'
+- './ee/spec/workers/geo/batch/project_registry_scheduler_worker_spec.rb'
+- './ee/spec/workers/geo/batch/project_registry_worker_spec.rb'
+- './ee/spec/workers/geo/container_repository_sync_dispatch_worker_spec.rb'
+- './ee/spec/workers/geo/container_repository_sync_worker_spec.rb'
+- './ee/spec/workers/geo/create_repository_updated_event_worker_spec.rb'
+- './ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb'
+- './ee/spec/workers/geo/design_repository_sync_worker_spec.rb'
+- './ee/spec/workers/geo/destroy_worker_spec.rb'
+- './ee/spec/workers/geo/event_worker_spec.rb'
+- './ee/spec/workers/geo/metrics_update_worker_spec.rb'
+- './ee/spec/workers/geo/project_sync_worker_spec.rb'
+- './ee/spec/workers/geo/prune_event_log_worker_spec.rb'
+- './ee/spec/workers/geo/registry_sync_worker_spec.rb'
+- './ee/spec/workers/geo/repositories_clean_up_worker_spec.rb'
+- './ee/spec/workers/geo/repository_cleanup_worker_spec.rb'
+- './ee/spec/workers/geo_repository_destroy_worker_spec.rb'
+- './ee/spec/workers/geo/repository_shard_sync_worker_spec.rb'
+- './ee/spec/workers/geo/repository_sync_worker_spec.rb'
+- './ee/spec/workers/geo/repository_verification/primary/batch_worker_spec.rb'
+- './ee/spec/workers/geo/repository_verification/primary/shard_worker_spec.rb'
+- './ee/spec/workers/geo/repository_verification/primary/single_worker_spec.rb'
+- './ee/spec/workers/geo/repository_verification/secondary/scheduler_worker_spec.rb'
+- './ee/spec/workers/geo/repository_verification/secondary/shard_worker_spec.rb'
+- './ee/spec/workers/geo/repository_verification/secondary/single_worker_spec.rb'
+- './ee/spec/workers/geo/reverification_batch_worker_spec.rb'
+- './ee/spec/workers/geo/scheduler/per_shard_scheduler_worker_spec.rb'
+- './ee/spec/workers/geo/scheduler/scheduler_worker_spec.rb'
+- './ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
+- './ee/spec/workers/geo/secondary_usage_data_cron_worker_spec.rb'
+- './ee/spec/workers/geo/sidekiq_cron_config_worker_spec.rb'
+- './ee/spec/workers/geo/sync_timeout_cron_worker_spec.rb'
+- './ee/spec/workers/geo/verification_batch_worker_spec.rb'
+- './ee/spec/workers/geo/verification_cron_worker_spec.rb'
+- './ee/spec/workers/geo/verification_state_backfill_service_spec.rb'
+- './ee/spec/workers/geo/verification_state_backfill_worker_spec.rb'
+- './ee/spec/workers/geo/verification_timeout_worker_spec.rb'
+- './ee/spec/workers/geo/verification_worker_spec.rb'
+- './ee/spec/workers/gitlab_subscriptions/notify_seats_exceeded_worker_spec.rb'
+- './ee/spec/workers/group_saml_group_sync_worker_spec.rb'
+- './ee/spec/workers/groups/create_event_worker_spec.rb'
+- './ee/spec/workers/groups/export_memberships_worker_spec.rb'
+- './ee/spec/workers/groups/schedule_bulk_repository_shard_moves_worker_spec.rb'
+- './ee/spec/workers/groups/update_repository_storage_worker_spec.rb'
+- './ee/spec/workers/group_wikis/git_garbage_collect_worker_spec.rb'
+- './ee/spec/workers/historical_data_worker_spec.rb'
+- './ee/spec/workers/import_software_licenses_worker_spec.rb'
+- './ee/spec/workers/incident_management/apply_incident_sla_exceeded_label_worker_spec.rb'
+- './ee/spec/workers/incident_management/incident_sla_exceeded_check_worker_spec.rb'
+- './ee/spec/workers/incident_management/oncall_rotations/persist_all_rotations_shifts_job_spec.rb'
+- './ee/spec/workers/incident_management/oncall_rotations/persist_shifts_job_spec.rb'
+- './ee/spec/workers/incident_management/pending_escalations/alert_check_worker_spec.rb'
+- './ee/spec/workers/incident_management/pending_escalations/alert_create_worker_spec.rb'
+- './ee/spec/workers/incident_management/pending_escalations/issue_check_worker_spec.rb'
+- './ee/spec/workers/incident_management/pending_escalations/issue_create_worker_spec.rb'
+- './ee/spec/workers/incident_management/pending_escalations/schedule_check_cron_worker_spec.rb'
+- './ee/spec/workers/iterations/cadences/create_iterations_worker_spec.rb'
+- './ee/spec/workers/iterations/cadences/schedule_create_iterations_worker_spec.rb'
+- './ee/spec/workers/iterations/roll_over_issues_worker_spec.rb'
+- './ee/spec/workers/iterations_update_status_worker_spec.rb'
+- './ee/spec/workers/ldap_all_groups_sync_worker_spec.rb'
+- './ee/spec/workers/ldap_group_sync_worker_spec.rb'
+- './ee/spec/workers/ldap_sync_worker_spec.rb'
+- './ee/spec/workers/licenses/reset_submit_license_usage_data_banner_worker_spec.rb'
+- './ee/spec/workers/merge_request_reset_approvals_worker_spec.rb'
+- './ee/spec/workers/merge_requests/stream_approval_audit_event_worker_spec.rb'
+- './ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb'
+- './ee/spec/workers/namespaces/free_user_cap/remediation_worker_spec.rb'
+- './ee/spec/workers/namespaces/sync_namespace_name_worker_spec.rb'
+- './ee/spec/workers/new_epic_worker_spec.rb'
+- './ee/spec/workers/personal_access_tokens/groups/policy_worker_spec.rb'
+- './ee/spec/workers/personal_access_tokens/instance/policy_worker_spec.rb'
+- './ee/spec/workers/post_receive_spec.rb'
+- './ee/spec/workers/project_cache_worker_spec.rb'
+- './ee/spec/workers/project_import_schedule_worker_spec.rb'
+- './ee/spec/workers/projects/disable_legacy_open_source_license_for_inactive_projects_worker_spec.rb'
+- './ee/spec/workers/project_template_export_worker_spec.rb'
+- './ee/spec/workers/refresh_license_compliance_checks_worker_spec.rb'
+- './ee/spec/workers/repository_import_worker_spec.rb'
+- './ee/spec/workers/repository_update_mirror_worker_spec.rb'
+- './ee/spec/workers/requirements_management/import_requirements_csv_worker_spec.rb'
+- './ee/spec/workers/requirements_management/process_requirements_reports_worker_spec.rb'
+- './ee/spec/workers/sbom/ingest_reports_worker_spec.rb'
+- './ee/spec/workers/scan_security_report_secrets_worker_spec.rb'
+- './ee/spec/workers/security/auto_fix_worker_spec.rb'
+- './ee/spec/workers/security/create_orchestration_policy_worker_spec.rb'
+- './ee/spec/workers/security/findings/cleanup_worker_spec.rb'
+- './ee/spec/workers/security/findings/delete_by_job_id_worker_spec.rb'
+- './ee/spec/workers/security/orchestration_policy_rule_schedule_namespace_worker_spec.rb'
+- './ee/spec/workers/security/orchestration_policy_rule_schedule_worker_spec.rb'
+- './ee/spec/workers/security/store_scans_worker_spec.rb'
+- './ee/spec/workers/security/sync_scan_policies_worker_spec.rb'
+- './ee/spec/workers/security/track_secure_scans_worker_spec.rb'
+- './ee/spec/workers/set_user_status_based_on_user_cap_setting_worker_spec.rb'
+- './ee/spec/workers/status_page/publish_worker_spec.rb'
+- './ee/spec/workers/store_security_reports_worker_spec.rb'
+- './ee/spec/workers/sync_seat_link_request_worker_spec.rb'
+- './ee/spec/workers/sync_seat_link_worker_spec.rb'
+- './ee/spec/workers/todos_destroyer/confidential_epic_worker_spec.rb'
+- './ee/spec/workers/update_all_mirrors_worker_spec.rb'
+- './ee/spec/workers/update_max_seats_used_for_gitlab_com_subscriptions_worker_spec.rb'
+- './ee/spec/workers/vulnerabilities/historical_statistics/deletion_worker_spec.rb'
+- './ee/spec/workers/vulnerabilities/statistics/adjustment_worker_spec.rb'
+- './ee/spec/workers/vulnerabilities/statistics/schedule_worker_spec.rb'
+- './ee/spec/workers/vulnerability_exports/export_deletion_worker_spec.rb'
+- './ee/spec/workers/vulnerability_exports/export_worker_spec.rb'
+- './spec/bin/feature_flag_spec.rb'
+- './spec/bin/sidekiq_cluster_spec.rb'
+- './spec/channels/application_cable/connection_spec.rb'
+- './spec/channels/awareness_channel_spec.rb'
+- './spec/commands/metrics_server/metrics_server_spec.rb'
+- './spec/commands/sidekiq_cluster/cli_spec.rb'
+- './spec/components/diffs/overflow_warning_component_spec.rb'
+- './spec/components/diffs/stats_component_spec.rb'
+- './spec/components/layouts/horizontal_section_component_spec.rb'
+- './spec/components/pajamas/alert_component_spec.rb'
+- './spec/components/pajamas/avatar_component_spec.rb'
+- './spec/components/pajamas/banner_component_spec.rb'
+- './spec/components/pajamas/button_component_spec.rb'
+- './spec/components/pajamas/card_component_spec.rb'
+- './spec/components/pajamas/checkbox_component_spec.rb'
+- './spec/components/pajamas/checkbox_tag_component_spec.rb'
+- './spec/components/pajamas/component_spec.rb'
+- './spec/components/pajamas/concerns/checkbox_radio_label_with_help_text_spec.rb'
+- './spec/components/pajamas/concerns/checkbox_radio_options_spec.rb'
+- './spec/components/pajamas/radio_component_spec.rb'
+- './spec/components/pajamas/spinner_component_spec.rb'
+- './spec/components/pajamas/toggle_component_spec.rb'
+- './spec/config/application_spec.rb'
+- './spec/config/inject_enterprise_edition_module_spec.rb'
+- './spec/config/mail_room_spec.rb'
+- './spec/config/metrics/aggregates/aggregated_metrics_spec.rb'
+- './spec/config/object_store_settings_spec.rb'
+- './spec/config/settings_spec.rb'
+- './spec/config/smime_signature_settings_spec.rb'
+- './spec/controllers/acme_challenges_controller_spec.rb'
+- './spec/controllers/admin/applications_controller_spec.rb'
+- './spec/controllers/admin/application_settings/appearances_controller_spec.rb'
+- './spec/controllers/admin/application_settings_controller_spec.rb'
+- './spec/controllers/admin/ci/variables_controller_spec.rb'
+- './spec/controllers/admin/clusters_controller_spec.rb'
+- './spec/controllers/admin/cohorts_controller_spec.rb'
+- './spec/controllers/admin/dashboard_controller_spec.rb'
+- './spec/controllers/admin/dev_ops_report_controller_spec.rb'
+- './spec/controllers/admin/gitaly_servers_controller_spec.rb'
+- './spec/controllers/admin/groups_controller_spec.rb'
+- './spec/controllers/admin/health_check_controller_spec.rb'
+- './spec/controllers/admin/hooks_controller_spec.rb'
+- './spec/controllers/admin/identities_controller_spec.rb'
+- './spec/controllers/admin/impersonations_controller_spec.rb'
+- './spec/controllers/admin/instance_review_controller_spec.rb'
+- './spec/controllers/admin/integrations_controller_spec.rb'
+- './spec/controllers/admin/jobs_controller_spec.rb'
+- './spec/controllers/admin/plan_limits_controller_spec.rb'
+- './spec/controllers/admin/projects_controller_spec.rb'
+- './spec/controllers/admin/runner_projects_controller_spec.rb'
+- './spec/controllers/admin/runners_controller_spec.rb'
+- './spec/controllers/admin/sessions_controller_spec.rb'
+- './spec/controllers/admin/spam_logs_controller_spec.rb'
+- './spec/controllers/admin/topics/avatars_controller_spec.rb'
+- './spec/controllers/admin/topics_controller_spec.rb'
+- './spec/controllers/admin/usage_trends_controller_spec.rb'
+- './spec/controllers/admin/users_controller_spec.rb'
+- './spec/controllers/application_controller_spec.rb'
+- './spec/controllers/autocomplete_controller_spec.rb'
+- './spec/controllers/boards/issues_controller_spec.rb'
+- './spec/controllers/boards/lists_controller_spec.rb'
+- './spec/controllers/chaos_controller_spec.rb'
+- './spec/controllers/concerns/boards_responses_spec.rb'
+- './spec/controllers/concerns/check_rate_limit_spec.rb'
+- './spec/controllers/concerns/checks_collaboration_spec.rb'
+- './spec/controllers/concerns/confirm_email_warning_spec.rb'
+- './spec/controllers/concerns/continue_params_spec.rb'
+- './spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb'
+- './spec/controllers/concerns/enforces_admin_authentication_spec.rb'
+- './spec/controllers/concerns/graceful_timeout_handling_spec.rb'
+- './spec/controllers/concerns/group_tree_spec.rb'
+- './spec/controllers/concerns/harbor/artifact_spec.rb'
+- './spec/controllers/concerns/harbor/repository_spec.rb'
+- './spec/controllers/concerns/harbor/tag_spec.rb'
+- './spec/controllers/concerns/import_url_params_spec.rb'
+- './spec/controllers/concerns/internal_redirect_spec.rb'
+- './spec/controllers/concerns/issuable_actions_spec.rb'
+- './spec/controllers/concerns/issuable_collections_spec.rb'
+- './spec/controllers/concerns/metrics_dashboard_spec.rb'
+- './spec/controllers/concerns/page_limiter_spec.rb'
+- './spec/controllers/concerns/product_analytics_tracking_spec.rb'
+- './spec/controllers/concerns/project_unauthorized_spec.rb'
+- './spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb'
+- './spec/controllers/concerns/redis_tracking_spec.rb'
+- './spec/controllers/concerns/renders_commits_spec.rb'
+- './spec/controllers/concerns/routable_actions_spec.rb'
+- './spec/controllers/concerns/send_file_upload_spec.rb'
+- './spec/controllers/concerns/sorting_preference_spec.rb'
+- './spec/controllers/concerns/sourcegraph_decorator_spec.rb'
+- './spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb'
+- './spec/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support_spec.rb'
+- './spec/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support_spec.rb'
+- './spec/controllers/concerns/spammable_actions/captcha_check/rest_api_actions_support_spec.rb'
+- './spec/controllers/concerns/static_object_external_storage_spec.rb'
+- './spec/controllers/confirmations_controller_spec.rb'
+- './spec/controllers/dashboard_controller_spec.rb'
+- './spec/controllers/dashboard/groups_controller_spec.rb'
+- './spec/controllers/dashboard/labels_controller_spec.rb'
+- './spec/controllers/dashboard/milestones_controller_spec.rb'
+- './spec/controllers/dashboard/projects_controller_spec.rb'
+- './spec/controllers/dashboard/snippets_controller_spec.rb'
+- './spec/controllers/dashboard/todos_controller_spec.rb'
+- './spec/controllers/every_controller_spec.rb'
+- './spec/controllers/explore/groups_controller_spec.rb'
+- './spec/controllers/explore/projects_controller_spec.rb'
+- './spec/controllers/explore/snippets_controller_spec.rb'
+- './spec/controllers/google_api/authorizations_controller_spec.rb'
+- './spec/controllers/graphql_controller_spec.rb'
+- './spec/controllers/groups/avatars_controller_spec.rb'
+- './spec/controllers/groups/boards_controller_spec.rb'
+- './spec/controllers/groups/children_controller_spec.rb'
+- './spec/controllers/groups/clusters_controller_spec.rb'
+- './spec/controllers/groups_controller_spec.rb'
+- './spec/controllers/groups/dependency_proxies_controller_spec.rb'
+- './spec/controllers/groups/dependency_proxy_auth_controller_spec.rb'
+- './spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb'
+- './spec/controllers/groups/group_links_controller_spec.rb'
+- './spec/controllers/groups/group_members_controller_spec.rb'
+- './spec/controllers/groups/imports_controller_spec.rb'
+- './spec/controllers/groups/labels_controller_spec.rb'
+- './spec/controllers/groups/milestones_controller_spec.rb'
+- './spec/controllers/groups/packages_controller_spec.rb'
+- './spec/controllers/groups/registry/repositories_controller_spec.rb'
+- './spec/controllers/groups/releases_controller_spec.rb'
+- './spec/controllers/groups/runners_controller_spec.rb'
+- './spec/controllers/groups/settings/applications_controller_spec.rb'
+- './spec/controllers/groups/settings/ci_cd_controller_spec.rb'
+- './spec/controllers/groups/settings/integrations_controller_spec.rb'
+- './spec/controllers/groups/settings/repository_controller_spec.rb'
+- './spec/controllers/groups/shared_projects_controller_spec.rb'
+- './spec/controllers/groups/uploads_controller_spec.rb'
+- './spec/controllers/groups/variables_controller_spec.rb'
+- './spec/controllers/health_check_controller_spec.rb'
+- './spec/controllers/help_controller_spec.rb'
+- './spec/controllers/import/available_namespaces_controller_spec.rb'
+- './spec/controllers/import/bitbucket_controller_spec.rb'
+- './spec/controllers/import/bitbucket_server_controller_spec.rb'
+- './spec/controllers/import/bulk_imports_controller_spec.rb'
+- './spec/controllers/import/fogbugz_controller_spec.rb'
+- './spec/controllers/import/gitea_controller_spec.rb'
+- './spec/controllers/import/github_controller_spec.rb'
+- './spec/controllers/import/gitlab_controller_spec.rb'
+- './spec/controllers/import/manifest_controller_spec.rb'
+- './spec/controllers/import/phabricator_controller_spec.rb'
+- './spec/controllers/invites_controller_spec.rb'
+- './spec/controllers/jira_connect/app_descriptor_controller_spec.rb'
+- './spec/controllers/jira_connect/branches_controller_spec.rb'
+- './spec/controllers/jira_connect/events_controller_spec.rb'
+- './spec/controllers/jira_connect/subscriptions_controller_spec.rb'
+- './spec/controllers/ldap/omniauth_callbacks_controller_spec.rb'
+- './spec/controllers/metrics_controller_spec.rb'
+- './spec/controllers/oauth/applications_controller_spec.rb'
+- './spec/controllers/oauth/authorizations_controller_spec.rb'
+- './spec/controllers/oauth/authorized_applications_controller_spec.rb'
+- './spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb'
+- './spec/controllers/oauth/token_info_controller_spec.rb'
+- './spec/controllers/oauth/tokens_controller_spec.rb'
+- './spec/controllers/omniauth_callbacks_controller_spec.rb'
+- './spec/controllers/passwords_controller_spec.rb'
+- './spec/controllers/profiles/accounts_controller_spec.rb'
+- './spec/controllers/profiles/active_sessions_controller_spec.rb'
+- './spec/controllers/profiles/avatars_controller_spec.rb'
+- './spec/controllers/profiles_controller_spec.rb'
+- './spec/controllers/profiles/emails_controller_spec.rb'
+- './spec/controllers/profiles/gpg_keys_controller_spec.rb'
+- './spec/controllers/profiles/keys_controller_spec.rb'
+- './spec/controllers/profiles/notifications_controller_spec.rb'
+- './spec/controllers/profiles/personal_access_tokens_controller_spec.rb'
+- './spec/controllers/profiles/preferences_controller_spec.rb'
+- './spec/controllers/profiles/two_factor_auths_controller_spec.rb'
+- './spec/controllers/profiles/webauthn_registrations_controller_spec.rb'
+- './spec/controllers/projects/alerting/notifications_controller_spec.rb'
+- './spec/controllers/projects/alert_management_controller_spec.rb'
+- './spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb'
+- './spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb'
+- './spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb'
+- './spec/controllers/projects/artifacts_controller_spec.rb'
+- './spec/controllers/projects/autocomplete_sources_controller_spec.rb'
+- './spec/controllers/projects/avatars_controller_spec.rb'
+- './spec/controllers/projects/badges_controller_spec.rb'
+- './spec/controllers/projects/blame_controller_spec.rb'
+- './spec/controllers/projects/blob_controller_spec.rb'
+- './spec/controllers/projects/boards_controller_spec.rb'
+- './spec/controllers/projects/branches_controller_spec.rb'
+- './spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb'
+- './spec/controllers/projects/ci/lints_controller_spec.rb'
+- './spec/controllers/projects/ci/pipeline_editor_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/compare_controller_spec.rb'
+- './spec/controllers/projects_controller_spec.rb'
+- './spec/controllers/projects/cycle_analytics_controller_spec.rb'
+- './spec/controllers/projects/cycle_analytics/events_controller_spec.rb'
+- './spec/controllers/projects/deploy_keys_controller_spec.rb'
+- './spec/controllers/projects/deployments_controller_spec.rb'
+- './spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb'
+- './spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb'
+- './spec/controllers/projects/discussions_controller_spec.rb'
+- './spec/controllers/projects/environments_controller_spec.rb'
+- './spec/controllers/projects/environments/prometheus_api_controller_spec.rb'
+- './spec/controllers/projects/environments/sample_metrics_controller_spec.rb'
+- './spec/controllers/projects/error_tracking_controller_spec.rb'
+- './spec/controllers/projects/error_tracking/projects_controller_spec.rb'
+- './spec/controllers/projects/error_tracking/stack_traces_controller_spec.rb'
+- './spec/controllers/projects/feature_flags_clients_controller_spec.rb'
+- './spec/controllers/projects/feature_flags_controller_spec.rb'
+- './spec/controllers/projects/feature_flags_user_lists_controller_spec.rb'
+- './spec/controllers/projects/find_file_controller_spec.rb'
+- './spec/controllers/projects/forks_controller_spec.rb'
+- './spec/controllers/projects/grafana_api_controller_spec.rb'
+- './spec/controllers/projects/graphs_controller_spec.rb'
+- './spec/controllers/projects/group_links_controller_spec.rb'
+- './spec/controllers/projects/hooks_controller_spec.rb'
+- './spec/controllers/projects/import/jira_controller_spec.rb'
+- './spec/controllers/projects/imports_controller_spec.rb'
+- './spec/controllers/projects/incidents_controller_spec.rb'
+- './spec/controllers/projects/issue_links_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/learn_gitlab_controller_spec.rb'
+- './spec/controllers/projects/mattermosts_controller_spec.rb'
+- './spec/controllers/projects/merge_requests/conflicts_controller_spec.rb'
+- './spec/controllers/projects/merge_requests/content_controller_spec.rb'
+- './spec/controllers/projects/merge_requests_controller_spec.rb'
+- './spec/controllers/projects/merge_requests/creations_controller_spec.rb'
+- './spec/controllers/projects/merge_requests/diffs_controller_spec.rb'
+- './spec/controllers/projects/merge_requests/drafts_controller_spec.rb'
+- './spec/controllers/projects/milestones_controller_spec.rb'
+- './spec/controllers/projects/mirrors_controller_spec.rb'
+- './spec/controllers/projects/notes_controller_spec.rb'
+- './spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb'
+- './spec/controllers/projects/packages/packages_controller_spec.rb'
+- './spec/controllers/projects/pages_controller_spec.rb'
+- './spec/controllers/projects/pages_domains_controller_spec.rb'
+- './spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
+- './spec/controllers/projects/pipeline_schedules_controller_spec.rb'
+- './spec/controllers/projects/pipelines_controller_spec.rb'
+- './spec/controllers/projects/pipelines_settings_controller_spec.rb'
+- './spec/controllers/projects/pipelines/stages_controller_spec.rb'
+- './spec/controllers/projects/pipelines/tests_controller_spec.rb'
+- './spec/controllers/projects/product_analytics_controller_spec.rb'
+- './spec/controllers/projects/project_members_controller_spec.rb'
+- './spec/controllers/projects/prometheus/alerts_controller_spec.rb'
+- './spec/controllers/projects/prometheus/metrics_controller_spec.rb'
+- './spec/controllers/projects/protected_branches_controller_spec.rb'
+- './spec/controllers/projects/protected_tags_controller_spec.rb'
+- './spec/controllers/projects/raw_controller_spec.rb'
+- './spec/controllers/projects/refs_controller_spec.rb'
+- './spec/controllers/projects/registry/repositories_controller_spec.rb'
+- './spec/controllers/projects/registry/tags_controller_spec.rb'
+- './spec/controllers/projects/releases_controller_spec.rb'
+- './spec/controllers/projects/releases/evidences_controller_spec.rb'
+- './spec/controllers/projects/repositories_controller_spec.rb'
+- './spec/controllers/projects/runners_controller_spec.rb'
+- './spec/controllers/projects/security/configuration_controller_spec.rb'
+- './spec/controllers/projects/service_desk_controller_spec.rb'
+- './spec/controllers/projects/service_ping_controller_spec.rb'
+- './spec/controllers/projects/settings/ci_cd_controller_spec.rb'
+- './spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb'
+- './spec/controllers/projects/settings/integrations_controller_spec.rb'
+- './spec/controllers/projects/settings/operations_controller_spec.rb'
+- './spec/controllers/projects/settings/repository_controller_spec.rb'
+- './spec/controllers/projects/snippets/blobs_controller_spec.rb'
+- './spec/controllers/projects/snippets_controller_spec.rb'
+- './spec/controllers/projects/starrers_controller_spec.rb'
+- './spec/controllers/projects/tags_controller_spec.rb'
+- './spec/controllers/projects/templates_controller_spec.rb'
+- './spec/controllers/projects/terraform_controller_spec.rb'
+- './spec/controllers/projects/todos_controller_spec.rb'
+- './spec/controllers/projects/tree_controller_spec.rb'
+- './spec/controllers/projects/uploads_controller_spec.rb'
+- './spec/controllers/projects/usage_quotas_controller_spec.rb'
+- './spec/controllers/projects/variables_controller_spec.rb'
+- './spec/controllers/projects/web_ide_schemas_controller_spec.rb'
+- './spec/controllers/projects/web_ide_terminals_controller_spec.rb'
+- './spec/controllers/projects/wikis_controller_spec.rb'
+- './spec/controllers/registrations_controller_spec.rb'
+- './spec/controllers/registrations/welcome_controller_spec.rb'
+- './spec/controllers/repositories/git_http_controller_spec.rb'
+- './spec/controllers/repositories/lfs_storage_controller_spec.rb'
+- './spec/controllers/root_controller_spec.rb'
+- './spec/controllers/search_controller_spec.rb'
+- './spec/controllers/sent_notifications_controller_spec.rb'
+- './spec/controllers/sessions_controller_spec.rb'
+- './spec/controllers/snippets/blobs_controller_spec.rb'
+- './spec/controllers/snippets_controller_spec.rb'
+- './spec/controllers/snippets/notes_controller_spec.rb'
+- './spec/controllers/uploads_controller_spec.rb'
+- './spec/controllers/users/callouts_controller_spec.rb'
+- './spec/controllers/users/terms_controller_spec.rb'
+- './spec/controllers/users/unsubscribes_controller_spec.rb'
+- './spec/db/development/create_base_work_item_types_spec.rb'
+- './spec/db/development/import_common_metrics_spec.rb'
+- './spec/db/docs_spec.rb'
+- './spec/db/migration_spec.rb'
+- './spec/db/production/create_base_work_item_types_spec.rb'
+- './spec/db/production/import_common_metrics_spec.rb'
+- './spec/db/production/settings_spec.rb'
+- './spec/db/schema_spec.rb'
+- './spec/dependencies/omniauth_saml_spec.rb'
+- './spec/experiments/application_experiment_spec.rb'
+- './spec/experiments/concerns/project_commit_count_spec.rb'
+- './spec/experiments/force_company_trial_experiment_spec.rb'
+- './spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb'
+- './spec/experiments/ios_specific_templates_experiment_spec.rb'
+- './spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb'
+- './spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb'
+- './spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb'
+- './spec/features/abuse_report_spec.rb'
+- './spec/features/action_cable_logging_spec.rb'
+- './spec/features/admin/admin_abuse_reports_spec.rb'
+- './spec/features/admin/admin_appearance_spec.rb'
+- './spec/features/admin/admin_broadcast_messages_spec.rb'
+- './spec/features/admin/admin_browse_spam_logs_spec.rb'
+- './spec/features/admin/admin_deploy_keys_spec.rb'
+- './spec/features/admin/admin_dev_ops_reports_spec.rb'
+- './spec/features/admin/admin_disables_git_access_protocol_spec.rb'
+- './spec/features/admin/admin_disables_two_factor_spec.rb'
+- './spec/features/admin/admin_groups_spec.rb'
+- './spec/features/admin/admin_health_check_spec.rb'
+- './spec/features/admin/admin_hook_logs_spec.rb'
+- './spec/features/admin/admin_hooks_spec.rb'
+- './spec/features/admin/admin_jobs_spec.rb'
+- './spec/features/admin/admin_labels_spec.rb'
+- './spec/features/admin/admin_manage_applications_spec.rb'
+- './spec/features/admin/admin_mode/login_spec.rb'
+- './spec/features/admin/admin_mode/logout_spec.rb'
+- './spec/features/admin/admin_mode_spec.rb'
+- './spec/features/admin/admin_mode/workers_spec.rb'
+- './spec/features/admin/admin_projects_spec.rb'
+- './spec/features/admin/admin_runners_spec.rb'
+- './spec/features/admin/admin_search_settings_spec.rb'
+- './spec/features/admin/admin_sees_background_migrations_spec.rb'
+- './spec/features/admin/admin_sees_projects_statistics_spec.rb'
+- './spec/features/admin/admin_sees_project_statistics_spec.rb'
+- './spec/features/admin/admin_settings_spec.rb'
+- './spec/features/admin/admin_system_info_spec.rb'
+- './spec/features/admin/admin_users_impersonation_tokens_spec.rb'
+- './spec/features/admin/admin_users_spec.rb'
+- './spec/features/admin/admin_uses_repository_checks_spec.rb'
+- './spec/features/admin/dashboard_spec.rb'
+- './spec/features/admin/integrations/instance_integrations_spec.rb'
+- './spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb'
+- './spec/features/admin/users/user_spec.rb'
+- './spec/features/admin/users/users_spec.rb'
+- './spec/features/admin_variables_spec.rb'
+- './spec/features/alert_management/alert_details_spec.rb'
+- './spec/features/alert_management/alert_management_list_spec.rb'
+- './spec/features/alert_management_spec.rb'
+- './spec/features/alert_management/user_filters_alerts_by_status_spec.rb'
+- './spec/features/alert_management/user_searches_alerts_spec.rb'
+- './spec/features/alert_management/user_updates_alert_status_spec.rb'
+- './spec/features/alerts_settings/user_views_alerts_settings_spec.rb'
+- './spec/features/atom/dashboard_issues_spec.rb'
+- './spec/features/atom/dashboard_spec.rb'
+- './spec/features/atom/issues_spec.rb'
+- './spec/features/atom/merge_requests_spec.rb'
+- './spec/features/atom/users_spec.rb'
+- './spec/features/boards/board_filters_spec.rb'
+- './spec/features/boards/boards_spec.rb'
+- './spec/features/boards/focus_mode_spec.rb'
+- './spec/features/boards/issue_ordering_spec.rb'
+- './spec/features/boards/keyboard_shortcut_spec.rb'
+- './spec/features/boards/multiple_boards_spec.rb'
+- './spec/features/boards/multi_select_spec.rb'
+- './spec/features/boards/new_issue_spec.rb'
+- './spec/features/boards/reload_boards_on_browser_back_spec.rb'
+- './spec/features/boards/sidebar_assignee_spec.rb'
+- './spec/features/boards/sidebar_labels_in_namespaces_spec.rb'
+- './spec/features/boards/sidebar_labels_spec.rb'
+- './spec/features/boards/sidebar_spec.rb'
+- './spec/features/boards/user_adds_lists_to_board_spec.rb'
+- './spec/features/boards/user_visits_board_spec.rb'
+- './spec/features/breadcrumbs_schema_markup_spec.rb'
+- './spec/features/broadcast_messages_spec.rb'
+- './spec/features/calendar_spec.rb'
+- './spec/features/callouts/registration_enabled_spec.rb'
+- './spec/features/canonical_link_spec.rb'
+- './spec/features/clusters/cluster_detail_page_spec.rb'
+- './spec/features/clusters/cluster_health_dashboard_spec.rb'
+- './spec/features/clusters/create_agent_spec.rb'
+- './spec/features/commit_spec.rb'
+- './spec/features/commits_spec.rb'
+- './spec/features/commits/user_uses_quick_actions_spec.rb'
+- './spec/features/commits/user_view_commits_spec.rb'
+- './spec/features/contextual_sidebar_spec.rb'
+- './spec/features/cycle_analytics_spec.rb'
+- './spec/features/dashboard/activity_spec.rb'
+- './spec/features/dashboard/archived_projects_spec.rb'
+- './spec/features/dashboard/datetime_on_tooltips_spec.rb'
+- './spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb'
+- './spec/features/dashboard/groups_list_spec.rb'
+- './spec/features/dashboard/group_spec.rb'
+- './spec/features/dashboard/issuables_counter_spec.rb'
+- './spec/features/dashboard/issues_filter_spec.rb'
+- './spec/features/dashboard/issues_spec.rb'
+- './spec/features/dashboard/label_filter_spec.rb'
+- './spec/features/dashboard/merge_requests_spec.rb'
+- './spec/features/dashboard/milestones_spec.rb'
+- './spec/features/dashboard/project_member_activity_index_spec.rb'
+- './spec/features/dashboard/projects_spec.rb'
+- './spec/features/dashboard/root_explore_spec.rb'
+- './spec/features/dashboard/shortcuts_spec.rb'
+- './spec/features/dashboard/snippets_spec.rb'
+- './spec/features/dashboard/todos/target_state_spec.rb'
+- './spec/features/dashboard/todos/todos_filtering_spec.rb'
+- './spec/features/dashboard/todos/todos_sorting_spec.rb'
+- './spec/features/dashboard/todos/todos_spec.rb'
+- './spec/features/dashboard/user_filters_projects_spec.rb'
+- './spec/features/discussion_comments/commit_spec.rb'
+- './spec/features/discussion_comments/issue_spec.rb'
+- './spec/features/discussion_comments/merge_request_spec.rb'
+- './spec/features/discussion_comments/snippets_spec.rb'
+- './spec/features/display_system_header_and_footer_bar_spec.rb'
+- './spec/features/error_pages_spec.rb'
+- './spec/features/error_tracking/user_filters_errors_by_status_spec.rb'
+- './spec/features/error_tracking/user_searches_sentry_errors_spec.rb'
+- './spec/features/error_tracking/user_sees_error_details_spec.rb'
+- './spec/features/error_tracking/user_sees_error_index_spec.rb'
+- './spec/features/expand_collapse_diffs_spec.rb'
+- './spec/features/explore/groups_list_spec.rb'
+- './spec/features/explore/groups_spec.rb'
+- './spec/features/explore/topics_spec.rb'
+- './spec/features/explore/user_explores_projects_spec.rb'
+- './spec/features/file_uploads/attachment_spec.rb'
+- './spec/features/file_uploads/ci_artifact_spec.rb'
+- './spec/features/file_uploads/git_lfs_spec.rb'
+- './spec/features/file_uploads/graphql_add_design_spec.rb'
+- './spec/features/file_uploads/group_import_spec.rb'
+- './spec/features/file_uploads/maven_package_spec.rb'
+- './spec/features/file_uploads/multipart_invalid_uploads_spec.rb'
+- './spec/features/file_uploads/nuget_package_spec.rb'
+- './spec/features/file_uploads/project_import_spec.rb'
+- './spec/features/file_uploads/rubygem_package_spec.rb'
+- './spec/features/file_uploads/user_avatar_spec.rb'
+- './spec/features/frequently_visited_projects_and_groups_spec.rb'
+- './spec/features/gitlab_experiments_spec.rb'
+- './spec/features/global_search_spec.rb'
+- './spec/features/graphiql_spec.rb'
+- './spec/features/graphql_known_operations_spec.rb'
+- './spec/features/groups/activity_spec.rb'
+- './spec/features/groups/board_sidebar_spec.rb'
+- './spec/features/groups/board_spec.rb'
+- './spec/features/groups/clusters/user_spec.rb'
+- './spec/features/groups/container_registry_spec.rb'
+- './spec/features/groups/crm/contacts/create_spec.rb'
+- './spec/features/groups/dependency_proxy_for_containers_spec.rb'
+- './spec/features/groups/dependency_proxy_spec.rb'
+- './spec/features/groups/empty_states_spec.rb'
+- './spec/features/groups/group_page_with_external_authorization_service_spec.rb'
+- './spec/features/groups/group_runners_spec.rb'
+- './spec/features/groups/group_settings_spec.rb'
+- './spec/features/groups/import_export/connect_instance_spec.rb'
+- './spec/features/groups/import_export/export_file_spec.rb'
+- './spec/features/groups/import_export/import_file_spec.rb'
+- './spec/features/groups/import_export/migration_history_spec.rb'
+- './spec/features/groups/integrations/group_integrations_spec.rb'
+- './spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb'
+- './spec/features/groups/issues_spec.rb'
+- './spec/features/groups/labels/create_spec.rb'
+- './spec/features/groups/labels/edit_spec.rb'
+- './spec/features/groups/labels/index_spec.rb'
+- './spec/features/groups/labels/search_labels_spec.rb'
+- './spec/features/groups/labels/sort_labels_spec.rb'
+- './spec/features/groups/labels/subscription_spec.rb'
+- './spec/features/groups/labels/user_sees_links_to_issuables_spec.rb'
+- './spec/features/groups/members/filter_members_spec.rb'
+- './spec/features/groups/members/leave_group_spec.rb'
+- './spec/features/groups/members/list_members_spec.rb'
+- './spec/features/groups/members/manage_groups_spec.rb'
+- './spec/features/groups/members/manage_members_spec.rb'
+- './spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb'
+- './spec/features/groups/members/master_manages_access_requests_spec.rb'
+- './spec/features/groups/members/request_access_spec.rb'
+- './spec/features/groups/members/search_members_spec.rb'
+- './spec/features/groups/members/sort_members_spec.rb'
+- './spec/features/groups/members/tabs_spec.rb'
+- './spec/features/groups/merge_requests_spec.rb'
+- './spec/features/groups/milestones/gfm_autocomplete_spec.rb'
+- './spec/features/groups/milestone_spec.rb'
+- './spec/features/groups/milestones_sorting_spec.rb'
+- './spec/features/groups/navbar_spec.rb'
+- './spec/features/groups/packages_spec.rb'
+- './spec/features/groups/settings/access_tokens_spec.rb'
+- './spec/features/groups/settings/ci_cd_spec.rb'
+- './spec/features/groups/settings/group_badges_spec.rb'
+- './spec/features/groups/settings/manage_applications_spec.rb'
+- './spec/features/groups/settings/packages_and_registries_spec.rb'
+- './spec/features/groups/settings/repository_spec.rb'
+- './spec/features/groups/settings/user_searches_in_settings_spec.rb'
+- './spec/features/groups/share_lock_spec.rb'
+- './spec/features/groups/show_spec.rb'
+- './spec/features/groups_spec.rb'
+- './spec/features/groups/user_browse_projects_group_page_spec.rb'
+- './spec/features/groups/user_sees_package_sidebar_spec.rb'
+- './spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb'
+- './spec/features/group_variables_spec.rb'
+- './spec/features/help_dropdown_spec.rb'
+- './spec/features/help_pages_spec.rb'
+- './spec/features/ics/dashboard_issues_spec.rb'
+- './spec/features/ics/group_issues_spec.rb'
+- './spec/features/ics/project_issues_spec.rb'
+- './spec/features/ide/clientside_preview_csp_spec.rb'
+- './spec/features/ide_spec.rb'
+- './spec/features/ide/static_object_external_storage_csp_spec.rb'
+- './spec/features/ide/user_commits_changes_spec.rb'
+- './spec/features/ide/user_opens_merge_request_spec.rb'
+- './spec/features/import/manifest_import_spec.rb'
+- './spec/features/incidents/incident_details_spec.rb'
+- './spec/features/incidents/incidents_list_spec.rb'
+- './spec/features/incidents/incident_timeline_events_spec.rb'
+- './spec/features/incidents/user_creates_new_incident_spec.rb'
+- './spec/features/incidents/user_filters_incidents_by_status_spec.rb'
+- './spec/features/incidents/user_searches_incidents_spec.rb'
+- './spec/features/incidents/user_views_incident_spec.rb'
+- './spec/features/invites_spec.rb'
+- './spec/features/issuables/issuable_list_spec.rb'
+- './spec/features/issuables/markdown_references/internal_references_spec.rb'
+- './spec/features/issuables/markdown_references/jira_spec.rb'
+- './spec/features/issuables/shortcuts_issuable_spec.rb'
+- './spec/features/issuables/sorting_list_spec.rb'
+- './spec/features/issuables/user_sees_sidebar_spec.rb'
+- './spec/features/issue_rebalancing_spec.rb'
+- './spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb'
+- './spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb'
+- './spec/features/issues/csv_spec.rb'
+- './spec/features/issues/discussion_lock_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_assignee_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_author_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_base_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_emoji_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_hint_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_label_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_milestone_spec.rb'
+- './spec/features/issues/filtered_search/dropdown_release_spec.rb'
+- './spec/features/issues/filtered_search/filter_issues_spec.rb'
+- './spec/features/issues/filtered_search/recent_searches_spec.rb'
+- './spec/features/issues/filtered_search/search_bar_spec.rb'
+- './spec/features/issues/filtered_search/visual_tokens_spec.rb'
+- './spec/features/issues/form_spec.rb'
+- './spec/features/issues/gfm_autocomplete_spec.rb'
+- './spec/features/issues/group_label_sidebar_spec.rb'
+- './spec/features/issues/incident_issue_spec.rb'
+- './spec/features/issues/issue_detail_spec.rb'
+- './spec/features/issues/issue_header_spec.rb'
+- './spec/features/issues/issue_sidebar_spec.rb'
+- './spec/features/issues/issue_state_spec.rb'
+- './spec/features/issues/keyboard_shortcut_spec.rb'
+- './spec/features/issues/markdown_toolbar_spec.rb'
+- './spec/features/issues/move_spec.rb'
+- './spec/features/issues/note_polling_spec.rb'
+- './spec/features/issues/notes_on_issues_spec.rb'
+- './spec/features/issues/related_issues_spec.rb'
+- './spec/features/issues/resource_label_events_spec.rb'
+- './spec/features/issues/rss_spec.rb'
+- './spec/features/issues/service_desk_spec.rb'
+- './spec/features/issues/spam_akismet_issue_creation_spec.rb'
+- './spec/features/issues/todo_spec.rb'
+- './spec/features/issues/user_bulk_edits_issues_labels_spec.rb'
+- './spec/features/issues/user_bulk_edits_issues_spec.rb'
+- './spec/features/issues/user_comments_on_issue_spec.rb'
+- './spec/features/issues/user_creates_branch_and_merge_request_spec.rb'
+- './spec/features/issues/user_creates_confidential_merge_request_spec.rb'
+- './spec/features/issues/user_creates_issue_by_email_spec.rb'
+- './spec/features/issues/user_creates_issue_spec.rb'
+- './spec/features/issues/user_edits_issue_spec.rb'
+- './spec/features/issues/user_filters_issues_spec.rb'
+- './spec/features/issues/user_interacts_with_awards_spec.rb'
+- './spec/features/issues/user_resets_their_incoming_email_token_spec.rb'
+- './spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb'
+- './spec/features/issues/user_sees_breadcrumb_links_spec.rb'
+- './spec/features/issues/user_sees_empty_state_spec.rb'
+- './spec/features/issues/user_sees_live_update_spec.rb'
+- './spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb'
+- './spec/features/issues/user_sorts_issue_comments_spec.rb'
+- './spec/features/issues/user_sorts_issues_spec.rb'
+- './spec/features/issues/user_toggles_subscription_spec.rb'
+- './spec/features/issues/user_uses_quick_actions_spec.rb'
+- './spec/features/issues/user_views_issue_spec.rb'
+- './spec/features/issues/user_views_issues_spec.rb'
+- './spec/features/jira_connect/branches_spec.rb'
+- './spec/features/jira_connect/subscriptions_spec.rb'
+- './spec/features/jira_oauth_provider_authorize_spec.rb'
+- './spec/features/labels_hierarchy_spec.rb'
+- './spec/features/markdown/copy_as_gfm_spec.rb'
+- './spec/features/markdown/gitlab_flavored_markdown_spec.rb'
+- './spec/features/markdown/json_table_spec.rb'
+- './spec/features/markdown/keyboard_shortcuts_spec.rb'
+- './spec/features/markdown/kroki_spec.rb'
+- './spec/features/markdown/markdown_spec.rb'
+- './spec/features/markdown/math_spec.rb'
+- './spec/features/markdown/metrics_spec.rb'
+- './spec/features/markdown/sandboxed_mermaid_spec.rb'
+- './spec/features/merge_request/batch_comments_spec.rb'
+- './spec/features/merge_request/close_reopen_report_toggle_spec.rb'
+- './spec/features/merge_request/maintainer_edits_fork_spec.rb'
+- './spec/features/merge_request/merge_request_discussion_lock_spec.rb'
+- './spec/features/merge_requests/filters_generic_behavior_spec.rb'
+- './spec/features/merge_requests/rss_spec.rb'
+- './spec/features/merge_requests/user_exports_as_csv_spec.rb'
+- './spec/features/merge_requests/user_filters_by_approvals_spec.rb'
+- './spec/features/merge_requests/user_filters_by_assignees_spec.rb'
+- './spec/features/merge_requests/user_filters_by_deployments_spec.rb'
+- './spec/features/merge_requests/user_filters_by_draft_spec.rb'
+- './spec/features/merge_requests/user_filters_by_labels_spec.rb'
+- './spec/features/merge_requests/user_filters_by_milestones_spec.rb'
+- './spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb'
+- './spec/features/merge_requests/user_filters_by_target_branch_spec.rb'
+- './spec/features/merge_requests/user_lists_merge_requests_spec.rb'
+- './spec/features/merge_requests/user_mass_updates_spec.rb'
+- './spec/features/merge_requests/user_sees_empty_state_spec.rb'
+- './spec/features/merge_requests/user_sorts_merge_requests_spec.rb'
+- './spec/features/merge_requests/user_views_all_merge_requests_spec.rb'
+- './spec/features/merge_requests/user_views_closed_merge_requests_spec.rb'
+- './spec/features/merge_requests/user_views_merged_merge_requests_spec.rb'
+- './spec/features/merge_requests/user_views_open_merge_requests_spec.rb'
+- './spec/features/merge_request/user_accepts_merge_request_spec.rb'
+- './spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb'
+- './spec/features/merge_request/user_approves_spec.rb'
+- './spec/features/merge_request/user_assigns_themselves_spec.rb'
+- './spec/features/merge_request/user_awards_emoji_spec.rb'
+- './spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb'
+- './spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb'
+- './spec/features/merge_request/user_comments_on_commit_spec.rb'
+- './spec/features/merge_request/user_comments_on_diff_spec.rb'
+- './spec/features/merge_request/user_comments_on_merge_request_spec.rb'
+- './spec/features/merge_request/user_creates_image_diff_notes_spec.rb'
+- './spec/features/merge_request/user_creates_merge_request_spec.rb'
+- './spec/features/merge_request/user_creates_mr_spec.rb'
+- './spec/features/merge_request/user_customizes_merge_commit_message_spec.rb'
+- './spec/features/merge_request/user_edits_assignees_sidebar_spec.rb'
+- './spec/features/merge_request/user_edits_merge_request_spec.rb'
+- './spec/features/merge_request/user_edits_mr_spec.rb'
+- './spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb'
+- './spec/features/merge_request/user_expands_diff_spec.rb'
+- './spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb'
+- './spec/features/merge_request/user_jumps_to_discussion_spec.rb'
+- './spec/features/merge_request/user_locks_discussion_spec.rb'
+- './spec/features/merge_request/user_manages_subscription_spec.rb'
+- './spec/features/merge_request/user_marks_merge_request_as_draft_spec.rb'
+- './spec/features/merge_request/user_merges_immediately_spec.rb'
+- './spec/features/merge_request/user_merges_merge_request_spec.rb'
+- './spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb'
+- './spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
+- './spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb'
+- './spec/features/merge_request/user_opens_context_commits_modal_spec.rb'
+- './spec/features/merge_request/user_posts_diff_notes_spec.rb'
+- './spec/features/merge_request/user_posts_notes_spec.rb'
+- './spec/features/merge_request/user_rebases_merge_request_spec.rb'
+- './spec/features/merge_request/user_resolves_conflicts_spec.rb'
+- './spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb'
+- './spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb'
+- './spec/features/merge_request/user_resolves_wip_mr_spec.rb'
+- './spec/features/merge_request/user_reverts_merge_request_spec.rb'
+- './spec/features/merge_request/user_reviews_image_spec.rb'
+- './spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb'
+- './spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb'
+- './spec/features/merge_request/user_sees_breadcrumb_links_spec.rb'
+- './spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb'
+- './spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb'
+- './spec/features/merge_request/user_sees_closing_issues_message_spec.rb'
+- './spec/features/merge_request/user_sees_deleted_target_branch_spec.rb'
+- './spec/features/merge_request/user_sees_deployment_widget_spec.rb'
+- './spec/features/merge_request/user_sees_diff_spec.rb'
+- './spec/features/merge_request/user_sees_discussions_spec.rb'
+- './spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb'
+- './spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb'
+- './spec/features/merge_request/user_sees_merge_widget_spec.rb'
+- './spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb'
+- './spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb'
+- './spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb'
+- './spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb'
+- './spec/features/merge_request/user_sees_page_metadata_spec.rb'
+- './spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb'
+- './spec/features/merge_request/user_sees_pipelines_spec.rb'
+- './spec/features/merge_request/user_sees_suggest_pipeline_spec.rb'
+- './spec/features/merge_request/user_sees_system_notes_spec.rb'
+- './spec/features/merge_request/user_sees_versions_spec.rb'
+- './spec/features/merge_request/user_sees_wip_help_message_spec.rb'
+- './spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb'
+- './spec/features/merge_request/user_squashes_merge_request_spec.rb'
+- './spec/features/merge_request/user_suggests_changes_on_diff_spec.rb'
+- './spec/features/merge_request/user_toggles_whitespace_changes_spec.rb'
+- './spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb'
+- './spec/features/merge_request/user_uses_quick_actions_spec.rb'
+- './spec/features/merge_request/user_views_auto_expanding_diff_spec.rb'
+- './spec/features/merge_request/user_views_diffs_commit_spec.rb'
+- './spec/features/merge_request/user_views_diffs_file_by_file_spec.rb'
+- './spec/features/merge_request/user_views_diffs_spec.rb'
+- './spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb'
+- './spec/features/merge_request/user_views_open_merge_request_spec.rb'
+- './spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb'
+- './spec/features/milestone_spec.rb'
+- './spec/features/milestones/user_creates_milestone_spec.rb'
+- './spec/features/milestones/user_deletes_milestone_spec.rb'
+- './spec/features/milestones/user_edits_milestone_spec.rb'
+- './spec/features/milestones/user_promotes_milestone_spec.rb'
+- './spec/features/milestones/user_sees_breadcrumb_links_spec.rb'
+- './spec/features/milestones/user_views_milestone_spec.rb'
+- './spec/features/milestones/user_views_milestones_spec.rb'
+- './spec/features/monitor_sidebar_link_spec.rb'
+- './spec/features/nav/top_nav_responsive_spec.rb'
+- './spec/features/nav/top_nav_tooltip_spec.rb'
+- './spec/features/oauth_login_spec.rb'
+- './spec/features/oauth_provider_authorize_spec.rb'
+- './spec/features/oauth_registration_spec.rb'
+- './spec/features/one_trust_spec.rb'
+- './spec/features/participants_autocomplete_spec.rb'
+- './spec/features/password_reset_spec.rb'
+- './spec/features/populate_new_pipeline_vars_with_params_spec.rb'
+- './spec/features/profiles/account_spec.rb'
+- './spec/features/profiles/active_sessions_spec.rb'
+- './spec/features/profiles/chat_names_spec.rb'
+- './spec/features/profiles/emails_spec.rb'
+- './spec/features/profiles/gpg_keys_spec.rb'
+- './spec/features/profiles/keys_spec.rb'
+- './spec/features/profiles/oauth_applications_spec.rb'
+- './spec/features/profiles/password_spec.rb'
+- './spec/features/profile_spec.rb'
+- './spec/features/profiles/personal_access_tokens_spec.rb'
+- './spec/features/profiles/two_factor_auths_spec.rb'
+- './spec/features/profiles/user_changes_notified_of_own_activity_spec.rb'
+- './spec/features/profiles/user_edit_preferences_spec.rb'
+- './spec/features/profiles/user_edit_profile_spec.rb'
+- './spec/features/profiles/user_manages_applications_spec.rb'
+- './spec/features/profiles/user_manages_emails_spec.rb'
+- './spec/features/profiles/user_search_settings_spec.rb'
+- './spec/features/profiles/user_visits_notifications_tab_spec.rb'
+- './spec/features/profiles/user_visits_profile_account_page_spec.rb'
+- './spec/features/profiles/user_visits_profile_authentication_log_spec.rb'
+- './spec/features/profiles/user_visits_profile_preferences_page_spec.rb'
+- './spec/features/profiles/user_visits_profile_spec.rb'
+- './spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb'
+- './spec/features/project_group_variables_spec.rb'
+- './spec/features/projects/active_tabs_spec.rb'
+- './spec/features/projects/activity/rss_spec.rb'
+- './spec/features/projects/activity/user_sees_activity_spec.rb'
+- './spec/features/projects/activity/user_sees_design_activity_spec.rb'
+- './spec/features/projects/activity/user_sees_design_comment_spec.rb'
+- './spec/features/projects/activity/user_sees_private_activity_spec.rb'
+- './spec/features/projects/artifacts/file_spec.rb'
+- './spec/features/projects/artifacts/raw_spec.rb'
+- './spec/features/projects/artifacts/user_browses_artifacts_spec.rb'
+- './spec/features/projects/artifacts/user_downloads_artifacts_spec.rb'
+- './spec/features/projects/badges/coverage_spec.rb'
+- './spec/features/projects/badges/list_spec.rb'
+- './spec/features/projects/badges/pipeline_badge_spec.rb'
+- './spec/features/projects/blobs/blame_spec.rb'
+- './spec/features/projects/blobs/blob_line_permalink_updater_spec.rb'
+- './spec/features/projects/blobs/blob_show_spec.rb'
+- './spec/features/projects/blobs/edit_spec.rb'
+- './spec/features/projects/blobs/shortcuts_blob_spec.rb'
+- './spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb'
+- './spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb'
+- './spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb'
+- './spec/features/projects/branches/download_buttons_spec.rb'
+- './spec/features/projects/branches/new_branch_ref_dropdown_spec.rb'
+- './spec/features/projects/branches_spec.rb'
+- './spec/features/projects/branches/user_creates_branch_spec.rb'
+- './spec/features/projects/branches/user_deletes_branch_spec.rb'
+- './spec/features/projects/branches/user_views_branches_spec.rb'
+- './spec/features/projects/ci/editor_spec.rb'
+- './spec/features/projects/ci/lint_spec.rb'
+- './spec/features/projects/classification_label_on_project_pages_spec.rb'
+- './spec/features/projects/cluster_agents_spec.rb'
+- './spec/features/projects/clusters/gcp_spec.rb'
+- './spec/features/projects/clusters_spec.rb'
+- './spec/features/projects/clusters/user_spec.rb'
+- './spec/features/projects/commit/builds_spec.rb'
+- './spec/features/projects/commit/cherry_pick_spec.rb'
+- './spec/features/projects/commit/comments/user_adds_comment_spec.rb'
+- './spec/features/projects/commit/comments/user_deletes_comments_spec.rb'
+- './spec/features/projects/commit/comments/user_edits_comments_spec.rb'
+- './spec/features/projects/commit/diff_notes_spec.rb'
+- './spec/features/projects/commit/mini_pipeline_graph_spec.rb'
+- './spec/features/projects/commits/multi_view_diff_spec.rb'
+- './spec/features/projects/commits/rss_spec.rb'
+- './spec/features/projects/commits/user_browses_commits_spec.rb'
+- './spec/features/projects/commit/user_comments_on_commit_spec.rb'
+- './spec/features/projects/commit/user_reverts_commit_spec.rb'
+- './spec/features/projects/commit/user_views_user_status_on_commit_spec.rb'
+- './spec/features/projects/compare_spec.rb'
+- './spec/features/projects/confluence/user_views_confluence_page_spec.rb'
+- './spec/features/projects/container_registry_spec.rb'
+- './spec/features/projects/deploy_keys_spec.rb'
+- './spec/features/projects/diffs/diff_show_spec.rb'
+- './spec/features/projects/environments/environment_metrics_spec.rb'
+- './spec/features/projects/environments/environment_spec.rb'
+- './spec/features/projects/environments/environments_spec.rb'
+- './spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb'
+- './spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb'
+- './spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb'
+- './spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb'
+- './spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb'
+- './spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb'
+- './spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb'
+- './spec/features/projects/features_visibility_spec.rb'
+- './spec/features/projects/files/dockerfile_dropdown_spec.rb'
+- './spec/features/projects/files/download_buttons_spec.rb'
+- './spec/features/projects/files/edit_file_soft_wrap_spec.rb'
+- './spec/features/projects/files/editing_a_file_spec.rb'
+- './spec/features/projects/files/files_sort_submodules_with_folders_spec.rb'
+- './spec/features/projects/files/find_file_keyboard_spec.rb'
+- './spec/features/projects/files/gitignore_dropdown_spec.rb'
+- './spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb'
+- './spec/features/projects/files/project_owner_creates_license_file_spec.rb'
+- './spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb'
+- './spec/features/projects/files/template_selector_menu_spec.rb'
+- './spec/features/projects/files/template_type_dropdown_spec.rb'
+- './spec/features/projects/files/undo_template_spec.rb'
+- './spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb'
+- './spec/features/projects/files/user_browses_files_spec.rb'
+- './spec/features/projects/files/user_browses_lfs_files_spec.rb'
+- './spec/features/projects/files/user_creates_directory_spec.rb'
+- './spec/features/projects/files/user_creates_files_spec.rb'
+- './spec/features/projects/files/user_deletes_files_spec.rb'
+- './spec/features/projects/files/user_edits_files_spec.rb'
+- './spec/features/projects/files/user_find_file_spec.rb'
+- './spec/features/projects/files/user_reads_pipeline_status_spec.rb'
+- './spec/features/projects/files/user_replaces_files_spec.rb'
+- './spec/features/projects/files/user_searches_for_files_spec.rb'
+- './spec/features/projects/files/user_uploads_files_spec.rb'
+- './spec/features/projects/forks/fork_list_spec.rb'
+- './spec/features/projects/fork_spec.rb'
+- './spec/features/projects/gfm_autocomplete_load_spec.rb'
+- './spec/features/projects/graph_spec.rb'
+- './spec/features/projects/hook_logs/user_reads_log_spec.rb'
+- './spec/features/projects/import_export/export_file_spec.rb'
+- './spec/features/projects/import_export/import_file_spec.rb'
+- './spec/features/projects/infrastructure_registry_spec.rb'
+- './spec/features/projects/integrations/disable_triggers_spec.rb'
+- './spec/features/projects/integrations/project_integrations_spec.rb'
+- './spec/features/projects/integrations/user_activates_asana_spec.rb'
+- './spec/features/projects/integrations/user_activates_assembla_spec.rb'
+- './spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb'
+- './spec/features/projects/integrations/user_activates_emails_on_push_spec.rb'
+- './spec/features/projects/integrations/user_activates_flowdock_spec.rb'
+- './spec/features/projects/integrations/user_activates_irker_spec.rb'
+- './spec/features/projects/integrations/user_activates_issue_tracker_spec.rb'
+- './spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb'
+- './spec/features/projects/integrations/user_activates_jira_spec.rb'
+- './spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb'
+- './spec/features/projects/integrations/user_activates_packagist_spec.rb'
+- './spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb'
+- './spec/features/projects/integrations/user_activates_prometheus_spec.rb'
+- './spec/features/projects/integrations/user_activates_pushover_spec.rb'
+- './spec/features/projects/integrations/user_activates_slack_notifications_spec.rb'
+- './spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb'
+- './spec/features/projects/integrations/user_uses_inherited_settings_spec.rb'
+- './spec/features/projects/integrations/user_views_services_spec.rb'
+- './spec/features/projects/issuable_templates_spec.rb'
+- './spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb'
+- './spec/features/projects/issues/design_management/user_paginates_designs_spec.rb'
+- './spec/features/projects/issues/design_management/user_permissions_upload_spec.rb'
+- './spec/features/projects/issues/design_management/user_uploads_designs_spec.rb'
+- './spec/features/projects/issues/design_management/user_views_design_images_spec.rb'
+- './spec/features/projects/issues/design_management/user_views_design_spec.rb'
+- './spec/features/projects/issues/design_management/user_views_designs_spec.rb'
+- './spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb'
+- './spec/features/projects/issues/email_participants_spec.rb'
+- './spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb'
+- './spec/features/projects/issues/viewing_relocated_issues_spec.rb'
+- './spec/features/projects/jobs/permissions_spec.rb'
+- './spec/features/projects/jobs_spec.rb'
+- './spec/features/projects/jobs/user_browses_job_spec.rb'
+- './spec/features/projects/jobs/user_browses_jobs_spec.rb'
+- './spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb'
+- './spec/features/projects/labels/issues_sorted_by_priority_spec.rb'
+- './spec/features/projects/labels/search_labels_spec.rb'
+- './spec/features/projects/labels/sort_labels_spec.rb'
+- './spec/features/projects/labels/subscription_spec.rb'
+- './spec/features/projects/labels/update_prioritization_spec.rb'
+- './spec/features/projects/labels/user_creates_labels_spec.rb'
+- './spec/features/projects/labels/user_edits_labels_spec.rb'
+- './spec/features/projects/labels/user_promotes_label_spec.rb'
+- './spec/features/projects/labels/user_removes_labels_spec.rb'
+- './spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb'
+- './spec/features/projects/labels/user_sees_links_to_issuables_spec.rb'
+- './spec/features/projects/labels/user_views_labels_spec.rb'
+- './spec/features/projects/members/anonymous_user_sees_members_spec.rb'
+- './spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb'
+- './spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb'
+- './spec/features/projects/members/group_members_spec.rb'
+- './spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb'
+- './spec/features/projects/members/groups_with_access_list_spec.rb'
+- './spec/features/projects/members/manage_groups_spec.rb'
+- './spec/features/projects/members/manage_members_spec.rb'
+- './spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb'
+- './spec/features/projects/members/master_manages_access_requests_spec.rb'
+- './spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb'
+- './spec/features/projects/members/member_leaves_project_spec.rb'
+- './spec/features/projects/members/owner_cannot_leave_project_spec.rb'
+- './spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb'
+- './spec/features/projects/members/sorting_spec.rb'
+- './spec/features/projects/members/tabs_spec.rb'
+- './spec/features/projects/members/user_requests_access_spec.rb'
+- './spec/features/projects/merge_request_button_spec.rb'
+- './spec/features/projects/milestones/gfm_autocomplete_spec.rb'
+- './spec/features/projects/milestones/milestone_spec.rb'
+- './spec/features/projects/milestones/milestones_sorting_spec.rb'
+- './spec/features/projects/milestones/new_spec.rb'
+- './spec/features/projects/milestones/user_interacts_with_labels_spec.rb'
+- './spec/features/projects/navbar_spec.rb'
+- './spec/features/projects/network_graph_spec.rb'
+- './spec/features/projects/new_project_from_template_spec.rb'
+- './spec/features/projects/new_project_spec.rb'
+- './spec/features/projects/package_files_spec.rb'
+- './spec/features/projects/packages_spec.rb'
+- './spec/features/projects/pages/user_adds_domain_spec.rb'
+- './spec/features/projects/pages/user_configures_pages_pipeline_spec.rb'
+- './spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb'
+- './spec/features/projects/pages/user_edits_settings_spec.rb'
+- './spec/features/projects/pipeline_schedules_spec.rb'
+- './spec/features/projects/pipelines/legacy_pipeline_spec.rb'
+- './spec/features/projects/pipelines/legacy_pipelines_spec.rb'
+- './spec/features/projects/pipelines/pipeline_spec.rb'
+- './spec/features/projects/pipelines/pipelines_spec.rb'
+- './spec/features/projects/product_analytics/events_spec.rb'
+- './spec/features/projects/product_analytics/graphs_spec.rb'
+- './spec/features/projects/product_analytics/setup_spec.rb'
+- './spec/features/projects/product_analytics/test_spec.rb'
+- './spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb'
+- './spec/features/projects/releases/user_creates_release_spec.rb'
+- './spec/features/projects/releases/user_views_edit_release_spec.rb'
+- './spec/features/projects/releases/user_views_release_spec.rb'
+- './spec/features/projects/releases/user_views_releases_spec.rb'
+- './spec/features/projects/remote_mirror_spec.rb'
+- './spec/features/projects/settings/access_tokens_spec.rb'
+- './spec/features/projects/settings/branch_rules_settings_spec.rb'
+- './spec/features/projects/settings/external_authorization_service_settings_spec.rb'
+- './spec/features/projects/settings/forked_project_settings_spec.rb'
+- './spec/features/projects/settings/lfs_settings_spec.rb'
+- './spec/features/projects/settings/monitor_settings_spec.rb'
+- './spec/features/projects/settings/packages_settings_spec.rb'
+- './spec/features/projects/settings/pipelines_settings_spec.rb'
+- './spec/features/projects/settings/project_badges_spec.rb'
+- './spec/features/projects/settings/project_settings_spec.rb'
+- './spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb'
+- './spec/features/projects/settings/registry_settings_spec.rb'
+- './spec/features/projects/settings/repository_settings_spec.rb'
+- './spec/features/projects/settings/secure_files_spec.rb'
+- './spec/features/projects/settings/service_desk_setting_spec.rb'
+- './spec/features/projects/settings/user_archives_project_spec.rb'
+- './spec/features/projects/settings/user_changes_avatar_spec.rb'
+- './spec/features/projects/settings/user_changes_default_branch_spec.rb'
+- './spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb'
+- './spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb'
+- './spec/features/projects/settings/user_manages_project_members_spec.rb'
+- './spec/features/projects/settings/user_renames_a_project_spec.rb'
+- './spec/features/projects/settings/user_searches_in_settings_spec.rb'
+- './spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb'
+- './spec/features/projects/settings/user_tags_project_spec.rb'
+- './spec/features/projects/settings/user_transfers_a_project_spec.rb'
+- './spec/features/projects/settings/visibility_settings_spec.rb'
+- './spec/features/projects/settings/webhooks_settings_spec.rb'
+- './spec/features/projects/show/download_buttons_spec.rb'
+- './spec/features/projects/show/no_password_spec.rb'
+- './spec/features/projects/show/redirects_spec.rb'
+- './spec/features/projects/show/rss_spec.rb'
+- './spec/features/projects/show/schema_markup_spec.rb'
+- './spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb'
+- './spec/features/projects/show/user_interacts_with_stars_spec.rb'
+- './spec/features/projects/show/user_manages_notifications_spec.rb'
+- './spec/features/projects/show/user_sees_collaboration_links_spec.rb'
+- './spec/features/projects/show/user_sees_deletion_failure_message_spec.rb'
+- './spec/features/projects/show/user_sees_git_instructions_spec.rb'
+- './spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb'
+- './spec/features/projects/show/user_sees_readme_spec.rb'
+- './spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb'
+- './spec/features/projects/show/user_uploads_files_spec.rb'
+- './spec/features/projects/snippets/create_snippet_spec.rb'
+- './spec/features/projects/snippets/show_spec.rb'
+- './spec/features/projects/snippets/user_comments_on_snippet_spec.rb'
+- './spec/features/projects/snippets/user_deletes_snippet_spec.rb'
+- './spec/features/projects/snippets/user_updates_snippet_spec.rb'
+- './spec/features/projects/snippets/user_views_snippets_spec.rb'
+- './spec/features/projects/sourcegraph_csp_spec.rb'
+- './spec/features/projects_spec.rb'
+- './spec/features/projects/sub_group_issuables_spec.rb'
+- './spec/features/projects/tags/download_buttons_spec.rb'
+- './spec/features/projects/tags/user_edits_tags_spec.rb'
+- './spec/features/projects/tags/user_views_tag_spec.rb'
+- './spec/features/projects/tags/user_views_tags_spec.rb'
+- './spec/features/projects/terraform_spec.rb'
+- './spec/features/projects/tree/create_directory_spec.rb'
+- './spec/features/projects/tree/create_file_spec.rb'
+- './spec/features/projects/tree/rss_spec.rb'
+- './spec/features/projects/tree/tree_show_spec.rb'
+- './spec/features/projects/tree/upload_file_spec.rb'
+- './spec/features/projects/user_changes_project_visibility_spec.rb'
+- './spec/features/projects/user_creates_project_spec.rb'
+- './spec/features/projects/user_sees_sidebar_spec.rb'
+- './spec/features/projects/user_sees_user_popover_spec.rb'
+- './spec/features/projects/user_sorts_projects_spec.rb'
+- './spec/features/projects/user_uses_shortcuts_spec.rb'
+- './spec/features/projects/user_views_empty_project_spec.rb'
+- './spec/features/projects/view_on_env_spec.rb'
+- './spec/features/projects/wikis_spec.rb'
+- './spec/features/projects/wiki/user_views_wiki_empty_spec.rb'
+- './spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb'
+- './spec/features/project_variables_spec.rb'
+- './spec/features/promotion_spec.rb'
+- './spec/features/protected_branches_spec.rb'
+- './spec/features/protected_tags_spec.rb'
+- './spec/features/read_only_spec.rb'
+- './spec/features/reportable_note/commit_spec.rb'
+- './spec/features/reportable_note/issue_spec.rb'
+- './spec/features/reportable_note/merge_request_spec.rb'
+- './spec/features/reportable_note/snippets_spec.rb'
+- './spec/features/runners_spec.rb'
+- './spec/features/search/user_searches_for_code_spec.rb'
+- './spec/features/search/user_searches_for_comments_spec.rb'
+- './spec/features/search/user_searches_for_commits_spec.rb'
+- './spec/features/search/user_searches_for_issues_spec.rb'
+- './spec/features/search/user_searches_for_merge_requests_spec.rb'
+- './spec/features/search/user_searches_for_milestones_spec.rb'
+- './spec/features/search/user_searches_for_projects_spec.rb'
+- './spec/features/search/user_searches_for_users_spec.rb'
+- './spec/features/search/user_searches_for_wiki_pages_spec.rb'
+- './spec/features/search/user_uses_header_search_field_spec.rb'
+- './spec/features/search/user_uses_search_filters_spec.rb'
+- './spec/features/security/admin_access_spec.rb'
+- './spec/features/security/dashboard_access_spec.rb'
+- './spec/features/security/group/internal_access_spec.rb'
+- './spec/features/security/group/private_access_spec.rb'
+- './spec/features/security/group/public_access_spec.rb'
+- './spec/features/security/profile_access_spec.rb'
+- './spec/features/security/project/internal_access_spec.rb'
+- './spec/features/security/project/private_access_spec.rb'
+- './spec/features/security/project/public_access_spec.rb'
+- './spec/features/security/project/snippet/internal_access_spec.rb'
+- './spec/features/security/project/snippet/private_access_spec.rb'
+- './spec/features/security/project/snippet/public_access_spec.rb'
+- './spec/features/sentry_js_spec.rb'
+- './spec/features/signed_commits_spec.rb'
+- './spec/features/snippets/embedded_snippet_spec.rb'
+- './spec/features/snippets/explore_spec.rb'
+- './spec/features/snippets/internal_snippet_spec.rb'
+- './spec/features/snippets/notes_on_personal_snippets_spec.rb'
+- './spec/features/snippets/private_snippets_spec.rb'
+- './spec/features/snippets/public_snippets_spec.rb'
+- './spec/features/snippets/search_snippets_spec.rb'
+- './spec/features/snippets/show_spec.rb'
+- './spec/features/snippets/spam_snippets_spec.rb'
+- './spec/features/snippets_spec.rb'
+- './spec/features/snippets/user_creates_snippet_spec.rb'
+- './spec/features/snippets/user_deletes_snippet_spec.rb'
+- './spec/features/snippets/user_edits_snippet_spec.rb'
+- './spec/features/snippets/user_snippets_spec.rb'
+- './spec/features/tags/developer_creates_tag_spec.rb'
+- './spec/features/tags/developer_deletes_tag_spec.rb'
+- './spec/features/tags/developer_views_tags_spec.rb'
+- './spec/features/tags/maintainer_deletes_protected_tag_spec.rb'
+- './spec/features/task_lists_spec.rb'
+- './spec/features/topic_show_spec.rb'
+- './spec/features/triggers_spec.rb'
+- './spec/features/u2f_spec.rb'
+- './spec/features/unsubscribe_links_spec.rb'
+- './spec/features/uploads/user_uploads_avatar_to_group_spec.rb'
+- './spec/features/uploads/user_uploads_avatar_to_profile_spec.rb'
+- './spec/features/uploads/user_uploads_file_to_note_spec.rb'
+- './spec/features/usage_stats_consent_spec.rb'
+- './spec/features/user_can_display_performance_bar_spec.rb'
+- './spec/features/user_opens_link_to_comment_spec.rb'
+- './spec/features/users/active_sessions_spec.rb'
+- './spec/features/users/add_email_to_existing_account_spec.rb'
+- './spec/features/users/anonymous_sessions_spec.rb'
+- './spec/features/users/bizible_csp_spec.rb'
+- './spec/features/users/confirmation_spec.rb'
+- './spec/features/user_sees_marketing_header_spec.rb'
+- './spec/features/user_sees_revert_modal_spec.rb'
+- './spec/features/users/email_verification_on_login_spec.rb'
+- './spec/features/users/google_analytics_csp_spec.rb'
+- './spec/features/users/login_spec.rb'
+- './spec/features/users/logout_spec.rb'
+- './spec/features/users/one_trust_csp_spec.rb'
+- './spec/features/user_sorts_things_spec.rb'
+- './spec/features/users/overview_spec.rb'
+- './spec/features/users/password_spec.rb'
+- './spec/features/users/rss_spec.rb'
+- './spec/features/users/show_spec.rb'
+- './spec/features/users/signup_spec.rb'
+- './spec/features/users/snippets_spec.rb'
+- './spec/features/users/terms_spec.rb'
+- './spec/features/users/user_browses_projects_on_user_page_spec.rb'
+- './spec/features/users/zuora_csp_spec.rb'
+- './spec/features/webauthn_spec.rb'
+- './spec/features/whats_new_spec.rb'
+- './spec/features/work_items/work_item_children_spec.rb'
+- './spec/finders/abuse_reports_finder_spec.rb'
+- './spec/finders/access_requests_finder_spec.rb'
+- './spec/finders/admin/plans_finder_spec.rb'
+- './spec/finders/admin/projects_finder_spec.rb'
+- './spec/finders/alert_management/alerts_finder_spec.rb'
+- './spec/finders/alert_management/http_integrations_finder_spec.rb'
+- './spec/finders/analytics/cycle_analytics/stage_finder_spec.rb'
+- './spec/finders/applications_finder_spec.rb'
+- './spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb'
+- './spec/finders/autocomplete/deploy_keys_with_write_access_finder_spec.rb'
+- './spec/finders/autocomplete/group_finder_spec.rb'
+- './spec/finders/autocomplete/move_to_project_finder_spec.rb'
+- './spec/finders/autocomplete/project_finder_spec.rb'
+- './spec/finders/autocomplete/routes_finder_spec.rb'
+- './spec/finders/autocomplete/users_finder_spec.rb'
+- './spec/finders/award_emojis_finder_spec.rb'
+- './spec/finders/boards/boards_finder_spec.rb'
+- './spec/finders/boards/visits_finder_spec.rb'
+- './spec/finders/branches_finder_spec.rb'
+- './spec/finders/bulk_imports/entities_finder_spec.rb'
+- './spec/finders/bulk_imports/imports_finder_spec.rb'
+- './spec/finders/ci/auth_job_finder_spec.rb'
+- './spec/finders/ci/commit_statuses_finder_spec.rb'
+- './spec/finders/ci/daily_build_group_report_results_finder_spec.rb'
+- './spec/finders/ci/job_artifacts_finder_spec.rb'
+- './spec/finders/ci/jobs_finder_spec.rb'
+- './spec/finders/ci/pipeline_schedules_finder_spec.rb'
+- './spec/finders/ci/pipelines_finder_spec.rb'
+- './spec/finders/ci/pipelines_for_merge_request_finder_spec.rb'
+- './spec/finders/ci/runner_jobs_finder_spec.rb'
+- './spec/finders/ci/runners_finder_spec.rb'
+- './spec/finders/ci/variables_finder_spec.rb'
+- './spec/finders/cluster_ancestors_finder_spec.rb'
+- './spec/finders/clusters/agent_authorizations_finder_spec.rb'
+- './spec/finders/clusters/agents_finder_spec.rb'
+- './spec/finders/clusters_finder_spec.rb'
+- './spec/finders/clusters/knative_services_finder_spec.rb'
+- './spec/finders/clusters/kubernetes_namespace_finder_spec.rb'
+- './spec/finders/concerns/finder_methods_spec.rb'
+- './spec/finders/concerns/finder_with_cross_project_access_spec.rb'
+- './spec/finders/concerns/finder_with_group_hierarchy_spec.rb'
+- './spec/finders/concerns/packages/finder_helper_spec.rb'
+- './spec/finders/container_repositories_finder_spec.rb'
+- './spec/finders/context_commits_finder_spec.rb'
+- './spec/finders/contributed_projects_finder_spec.rb'
+- './spec/finders/crm/contacts_finder_spec.rb'
+- './spec/finders/crm/organizations_finder_spec.rb'
+- './spec/finders/database/batched_background_migrations_finder_spec.rb'
+- './spec/finders/deployments_finder_spec.rb'
+- './spec/finders/deploy_tokens/tokens_finder_spec.rb'
+- './spec/finders/design_management/designs_finder_spec.rb'
+- './spec/finders/design_management/versions_finder_spec.rb'
+- './spec/finders/environments/environment_names_finder_spec.rb'
+- './spec/finders/environments/environments_by_deployments_finder_spec.rb'
+- './spec/finders/environments/environments_finder_spec.rb'
+- './spec/finders/events_finder_spec.rb'
+- './spec/finders/feature_flags_finder_spec.rb'
+- './spec/finders/feature_flags_user_lists_finder_spec.rb'
+- './spec/finders/fork_projects_finder_spec.rb'
+- './spec/finders/fork_targets_finder_spec.rb'
+- './spec/finders/freeze_periods_finder_spec.rb'
+- './spec/finders/group_descendants_finder_spec.rb'
+- './spec/finders/group_members_finder_spec.rb'
+- './spec/finders/group_projects_finder_spec.rb'
+- './spec/finders/groups/accepting_project_transfers_finder_spec.rb'
+- './spec/finders/groups_finder_spec.rb'
+- './spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb'
+- './spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb'
+- './spec/finders/groups/user_groups_finder_spec.rb'
+- './spec/finders/incident_management/timeline_events_finder_spec.rb'
+- './spec/finders/issuables/crm_contact_filter_spec.rb'
+- './spec/finders/issuables/crm_organization_filter_spec.rb'
+- './spec/finders/issues_finder_spec.rb'
+- './spec/finders/joined_groups_finder_spec.rb'
+- './spec/finders/keys_finder_spec.rb'
+- './spec/finders/labels_finder_spec.rb'
+- './spec/finders/lfs_pointers_finder_spec.rb'
+- './spec/finders/license_template_finder_spec.rb'
+- './spec/finders/members_finder_spec.rb'
+- './spec/finders/merge_request/metrics_finder_spec.rb'
+- './spec/finders/merge_requests/by_approvals_finder_spec.rb'
+- './spec/finders/merge_requests_finder/params_spec.rb'
+- './spec/finders/merge_requests_finder_spec.rb'
+- './spec/finders/merge_requests/oldest_per_commit_finder_spec.rb'
+- './spec/finders/merge_request_target_project_finder_spec.rb'
+- './spec/finders/metrics/dashboards/annotations_finder_spec.rb'
+- './spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
+- './spec/finders/milestones_finder_spec.rb'
+- './spec/finders/namespaces/projects_finder_spec.rb'
+- './spec/finders/notes_finder_spec.rb'
+- './spec/finders/packages/build_infos_finder_spec.rb'
+- './spec/finders/packages/composer/packages_finder_spec.rb'
+- './spec/finders/packages/conan/package_file_finder_spec.rb'
+- './spec/finders/packages/conan/package_finder_spec.rb'
+- './spec/finders/packages/debian/distributions_finder_spec.rb'
+- './spec/finders/packages/generic/package_finder_spec.rb'
+- './spec/finders/packages/go/module_finder_spec.rb'
+- './spec/finders/packages/go/package_finder_spec.rb'
+- './spec/finders/packages/go/version_finder_spec.rb'
+- './spec/finders/packages/group_or_project_package_finder_spec.rb'
+- './spec/finders/packages/group_packages_finder_spec.rb'
+- './spec/finders/packages/helm/package_files_finder_spec.rb'
+- './spec/finders/packages/helm/packages_finder_spec.rb'
+- './spec/finders/packages/maven/package_finder_spec.rb'
+- './spec/finders/packages/npm/package_finder_spec.rb'
+- './spec/finders/packages/nuget/package_finder_spec.rb'
+- './spec/finders/packages/package_file_finder_spec.rb'
+- './spec/finders/packages/package_finder_spec.rb'
+- './spec/finders/packages/packages_finder_spec.rb'
+- './spec/finders/packages/pypi/package_finder_spec.rb'
+- './spec/finders/packages/pypi/packages_finder_spec.rb'
+- './spec/finders/packages/tags_finder_spec.rb'
+- './spec/finders/pending_todos_finder_spec.rb'
+- './spec/finders/personal_access_tokens_finder_spec.rb'
+- './spec/finders/personal_projects_finder_spec.rb'
+- './spec/finders/projects/export_job_finder_spec.rb'
+- './spec/finders/projects_finder_spec.rb'
+- './spec/finders/projects/groups_finder_spec.rb'
+- './spec/finders/projects/members/effective_access_level_finder_spec.rb'
+- './spec/finders/projects/members/effective_access_level_per_user_finder_spec.rb'
+- './spec/finders/projects/prometheus/alerts_finder_spec.rb'
+- './spec/finders/projects/topics_finder_spec.rb'
+- './spec/finders/prometheus_metrics_finder_spec.rb'
+- './spec/finders/protected_branches_finder_spec.rb'
+- './spec/finders/releases/evidence_pipeline_finder_spec.rb'
+- './spec/finders/releases_finder_spec.rb'
+- './spec/finders/releases/group_releases_finder_spec.rb'
+- './spec/finders/repositories/branch_names_finder_spec.rb'
+- './spec/finders/repositories/changelog_commits_finder_spec.rb'
+- './spec/finders/repositories/changelog_tag_finder_spec.rb'
+- './spec/finders/repositories/tree_finder_spec.rb'
+- './spec/finders/resource_milestone_event_finder_spec.rb'
+- './spec/finders/resource_state_event_finder_spec.rb'
+- './spec/finders/security/jobs_finder_spec.rb'
+- './spec/finders/security/license_compliance_jobs_finder_spec.rb'
+- './spec/finders/security/security_jobs_finder_spec.rb'
+- './spec/finders/sentry_issue_finder_spec.rb'
+- './spec/finders/serverless_domain_finder_spec.rb'
+- './spec/finders/snippets_finder_spec.rb'
+- './spec/finders/starred_projects_finder_spec.rb'
+- './spec/finders/tags_finder_spec.rb'
+- './spec/finders/template_finder_spec.rb'
+- './spec/finders/terraform/states_finder_spec.rb'
+- './spec/finders/todos_finder_spec.rb'
+- './spec/finders/uploader_finder_spec.rb'
+- './spec/finders/user_finder_spec.rb'
+- './spec/finders/user_group_notification_settings_finder_spec.rb'
+- './spec/finders/user_groups_counter_spec.rb'
+- './spec/finders/user_recent_events_finder_spec.rb'
+- './spec/finders/users_finder_spec.rb'
+- './spec/finders/users_star_projects_finder_spec.rb'
+- './spec/finders/work_items/work_items_finder_spec.rb'
+- './spec/frontend/fixtures/abuse_reports.rb'
+- './spec/frontend/fixtures/admin_users.rb'
+- './spec/frontend/fixtures/analytics.rb'
+- './spec/frontend/fixtures/api_deploy_keys.rb'
+- './spec/frontend/fixtures/api_merge_requests.rb'
+- './spec/frontend/fixtures/api_projects.rb'
+- './spec/frontend/fixtures/application_settings.rb'
+- './spec/frontend/fixtures/autocomplete.rb'
+- './spec/frontend/fixtures/autocomplete_sources.rb'
+- './spec/frontend/fixtures/blob.rb'
+- './spec/frontend/fixtures/branches.rb'
+- './spec/frontend/fixtures/clusters.rb'
+- './spec/frontend/fixtures/commit.rb'
+- './spec/frontend/fixtures/deploy_keys.rb'
+- './spec/frontend/fixtures/freeze_period.rb'
+- './spec/frontend/fixtures/groups.rb'
+- './spec/frontend/fixtures/integrations.rb'
+- './spec/frontend/fixtures/issues.rb'
+- './spec/frontend/fixtures/jobs.rb'
+- './spec/frontend/fixtures/labels.rb'
+- './spec/frontend/fixtures/listbox.rb'
+- './spec/frontend/fixtures/merge_requests_diffs.rb'
+- './spec/frontend/fixtures/merge_requests.rb'
+- './spec/frontend/fixtures/metrics_dashboard.rb'
+- './spec/frontend/fixtures/namespaces.rb'
+- './spec/frontend/fixtures/pipeline_schedules.rb'
+- './spec/frontend/fixtures/pipelines.rb'
+- './spec/frontend/fixtures/projects_json.rb'
+- './spec/frontend/fixtures/projects.rb'
+- './spec/frontend/fixtures/prometheus_integration.rb'
+- './spec/frontend/fixtures/raw.rb'
+- './spec/frontend/fixtures/releases.rb'
+- './spec/frontend/fixtures/runner.rb'
+- './spec/frontend/fixtures/search.rb'
+- './spec/frontend/fixtures/sessions.rb'
+- './spec/frontend/fixtures/snippet.rb'
+- './spec/frontend/fixtures/startup_css.rb'
+- './spec/frontend/fixtures/tabs.rb'
+- './spec/frontend/fixtures/tags.rb'
+- './spec/frontend/fixtures/timezones.rb'
+- './spec/frontend/fixtures/todos.rb'
+- './spec/frontend/fixtures/u2f.rb'
+- './spec/frontend/fixtures/webauthn.rb'
+- './spec/graphql/features/authorization_spec.rb'
+- './spec/graphql/features/feature_flag_spec.rb'
+- './spec/graphql/gitlab_schema_spec.rb'
+- './spec/graphql/graphql_triggers_spec.rb'
+- './spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb'
+- './spec/graphql/mutations/alert_management/alerts/todo/create_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/graphql/mutations/alert_management/update_alert_status_spec.rb'
+- './spec/graphql/mutations/base_mutation_spec.rb'
+- './spec/graphql/mutations/boards/issues/issue_move_list_spec.rb'
+- './spec/graphql/mutations/boards/lists/create_spec.rb'
+- './spec/graphql/mutations/boards/lists/update_spec.rb'
+- './spec/graphql/mutations/boards/update_spec.rb'
+- './spec/graphql/mutations/branches/create_spec.rb'
+- './spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb'
+- './spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb'
+- './spec/graphql/mutations/ci/runner/bulk_delete_spec.rb'
+- './spec/graphql/mutations/ci/runner/delete_spec.rb'
+- './spec/graphql/mutations/ci/runner/update_spec.rb'
+- './spec/graphql/mutations/clusters/agents/create_spec.rb'
+- './spec/graphql/mutations/clusters/agents/delete_spec.rb'
+- './spec/graphql/mutations/clusters/agent_tokens/create_spec.rb'
+- './spec/graphql/mutations/clusters/agent_tokens/revoke_spec.rb'
+- './spec/graphql/mutations/commits/create_spec.rb'
+- './spec/graphql/mutations/concerns/mutations/finds_by_gid_spec.rb'
+- './spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb'
+- './spec/graphql/mutations/concerns/mutations/resolves_issuable_spec.rb'
+- './spec/graphql/mutations/container_expiration_policies/update_spec.rb'
+- './spec/graphql/mutations/container_repositories/destroy_spec.rb'
+- './spec/graphql/mutations/container_repositories/destroy_tags_spec.rb'
+- './spec/graphql/mutations/custom_emoji/create_spec.rb'
+- './spec/graphql/mutations/custom_emoji/destroy_spec.rb'
+- './spec/graphql/mutations/customer_relations/contacts/create_spec.rb'
+- './spec/graphql/mutations/customer_relations/contacts/update_spec.rb'
+- './spec/graphql/mutations/customer_relations/organizations/create_spec.rb'
+- './spec/graphql/mutations/customer_relations/organizations/update_spec.rb'
+- './spec/graphql/mutations/dependency_proxy/group_settings/update_spec.rb'
+- './spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb'
+- './spec/graphql/mutations/design_management/delete_spec.rb'
+- './spec/graphql/mutations/design_management/move_spec.rb'
+- './spec/graphql/mutations/design_management/upload_spec.rb'
+- './spec/graphql/mutations/discussions/toggle_resolve_spec.rb'
+- './spec/graphql/mutations/environments/canary_ingress/update_spec.rb'
+- './spec/graphql/mutations/groups/update_spec.rb'
+- './spec/graphql/mutations/incident_management/timeline_event/create_spec.rb'
+- './spec/graphql/mutations/incident_management/timeline_event/destroy_spec.rb'
+- './spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb'
+- './spec/graphql/mutations/incident_management/timeline_event/update_spec.rb'
+- './spec/graphql/mutations/issues/create_spec.rb'
+- './spec/graphql/mutations/issues/move_spec.rb'
+- './spec/graphql/mutations/issues/set_assignees_spec.rb'
+- './spec/graphql/mutations/issues/set_confidential_spec.rb'
+- './spec/graphql/mutations/issues/set_due_date_spec.rb'
+- './spec/graphql/mutations/issues/set_escalation_status_spec.rb'
+- './spec/graphql/mutations/issues/set_locked_spec.rb'
+- './spec/graphql/mutations/issues/set_severity_spec.rb'
+- './spec/graphql/mutations/issues/set_subscription_spec.rb'
+- './spec/graphql/mutations/issues/update_spec.rb'
+- './spec/graphql/mutations/labels/create_spec.rb'
+- './spec/graphql/mutations/merge_requests/accept_spec.rb'
+- './spec/graphql/mutations/merge_requests/create_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_assignees_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_draft_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_labels_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_locked_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_milestone_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_reviewers_spec.rb'
+- './spec/graphql/mutations/merge_requests/set_subscription_spec.rb'
+- './spec/graphql/mutations/merge_requests/update_spec.rb'
+- './spec/graphql/mutations/namespace/package_settings/update_spec.rb'
+- './spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb'
+- './spec/graphql/mutations/pages/mark_onboarding_complete_spec.rb'
+- './spec/graphql/mutations/release_asset_links/create_spec.rb'
+- './spec/graphql/mutations/release_asset_links/delete_spec.rb'
+- './spec/graphql/mutations/release_asset_links/update_spec.rb'
+- './spec/graphql/mutations/releases/create_spec.rb'
+- './spec/graphql/mutations/releases/delete_spec.rb'
+- './spec/graphql/mutations/releases/update_spec.rb'
+- './spec/graphql/mutations/saved_replies/create_spec.rb'
+- './spec/graphql/mutations/saved_replies/destroy_spec.rb'
+- './spec/graphql/mutations/saved_replies/update_spec.rb'
+- './spec/graphql/mutations/security/ci_configuration/base_security_analyzer_spec.rb'
+- './spec/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb'
+- './spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb'
+- './spec/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb'
+- './spec/graphql/mutations/terraform/state/delete_spec.rb'
+- './spec/graphql/mutations/terraform/state/lock_spec.rb'
+- './spec/graphql/mutations/terraform/state/unlock_spec.rb'
+- './spec/graphql/mutations/timelogs/delete_spec.rb'
+- './spec/graphql/mutations/todos/create_spec.rb'
+- './spec/graphql/mutations/todos/mark_all_done_spec.rb'
+- './spec/graphql/mutations/todos/mark_done_spec.rb'
+- './spec/graphql/mutations/todos/restore_many_spec.rb'
+- './spec/graphql/mutations/todos/restore_spec.rb'
+- './spec/graphql/mutations/user_callouts/create_spec.rb'
+- './spec/graphql/mutations/work_items/update_task_spec.rb'
+- './spec/graphql/mutations/work_items/update_widgets_spec.rb'
+- './spec/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver_spec.rb'
+- './spec/graphql/resolvers/alert_management/alert_resolver_spec.rb'
+- './spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb'
+- './spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb'
+- './spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb'
+- './spec/graphql/resolvers/base_resolver_spec.rb'
+- './spec/graphql/resolvers/blobs_resolver_spec.rb'
+- './spec/graphql/resolvers/board_list_issues_resolver_spec.rb'
+- './spec/graphql/resolvers/board_list_resolver_spec.rb'
+- './spec/graphql/resolvers/board_lists_resolver_spec.rb'
+- './spec/graphql/resolvers/board_resolver_spec.rb'
+- './spec/graphql/resolvers/boards_resolver_spec.rb'
+- './spec/graphql/resolvers/branch_commit_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/config_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/group_runners_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/jobs_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/runners_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/runner_status_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/template_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb'
+- './spec/graphql/resolvers/ci/test_suite_resolver_spec.rb'
+- './spec/graphql/resolvers/clusters/agent_activity_events_resolver_spec.rb'
+- './spec/graphql/resolvers/clusters/agents_resolver_spec.rb'
+- './spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb'
+- './spec/graphql/resolvers/commit_pipelines_resolver_spec.rb'
+- './spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb'
+- './spec/graphql/resolvers/concerns/looks_ahead_spec.rb'
+- './spec/graphql/resolvers/concerns/resolves_groups_spec.rb'
+- './spec/graphql/resolvers/concerns/resolves_ids_spec.rb'
+- './spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb'
+- './spec/graphql/resolvers/concerns/resolves_project_spec.rb'
+- './spec/graphql/resolvers/container_repositories_resolver_spec.rb'
+- './spec/graphql/resolvers/container_repository_tags_resolver_spec.rb'
+- './spec/graphql/resolvers/crm/contacts_resolver_spec.rb'
+- './spec/graphql/resolvers/crm/contact_state_counts_resolver_spec.rb'
+- './spec/graphql/resolvers/crm/organizations_resolver_spec.rb'
+- './spec/graphql/resolvers/crm/organization_state_counts_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/design_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/designs_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/version_resolver_spec.rb'
+- './spec/graphql/resolvers/design_management/versions_resolver_spec.rb'
+- './spec/graphql/resolvers/echo_resolver_spec.rb'
+- './spec/graphql/resolvers/environments_resolver_spec.rb'
+- './spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb'
+- './spec/graphql/resolvers/error_tracking/sentry_error_collection_resolver_spec.rb'
+- './spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb'
+- './spec/graphql/resolvers/group_issues_resolver_spec.rb'
+- './spec/graphql/resolvers/group_labels_resolver_spec.rb'
+- './spec/graphql/resolvers/group_members/notification_email_resolver_spec.rb'
+- './spec/graphql/resolvers/group_members_resolver_spec.rb'
+- './spec/graphql/resolvers/group_milestones_resolver_spec.rb'
+- './spec/graphql/resolvers/group_packages_resolver_spec.rb'
+- './spec/graphql/resolvers/group_resolver_spec.rb'
+- './spec/graphql/resolvers/groups_resolver_spec.rb'
+- './spec/graphql/resolvers/incident_management/timeline_events_resolver_spec.rb'
+- './spec/graphql/resolvers/issues_resolver_spec.rb'
+- './spec/graphql/resolvers/issue_status_counts_resolver_spec.rb'
+- './spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb'
+- './spec/graphql/resolvers/kas/agent_connections_resolver_spec.rb'
+- './spec/graphql/resolvers/labels_resolver_spec.rb'
+- './spec/graphql/resolvers/last_commit_resolver_spec.rb'
+- './spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb'
+- './spec/graphql/resolvers/merge_requests_count_resolver_spec.rb'
+- './spec/graphql/resolvers/merge_requests_resolver_spec.rb'
+- './spec/graphql/resolvers/metadata_resolver_spec.rb'
+- './spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb'
+- './spec/graphql/resolvers/metrics/dashboards/annotation_resolver_spec.rb'
+- './spec/graphql/resolvers/namespace_projects_resolver_spec.rb'
+- './spec/graphql/resolvers/package_details_resolver_spec.rb'
+- './spec/graphql/resolvers/package_pipelines_resolver_spec.rb'
+- './spec/graphql/resolvers/packages_base_resolver_spec.rb'
+- './spec/graphql/resolvers/paginated_tree_resolver_spec.rb'
+- './spec/graphql/resolvers/project_jobs_resolver_spec.rb'
+- './spec/graphql/resolvers/project_members_resolver_spec.rb'
+- './spec/graphql/resolvers/project_merge_requests_resolver_spec.rb'
+- './spec/graphql/resolvers/project_milestones_resolver_spec.rb'
+- './spec/graphql/resolvers/project_packages_resolver_spec.rb'
+- './spec/graphql/resolvers/project_pipeline_resolver_spec.rb'
+- './spec/graphql/resolvers/project_pipelines_resolver_spec.rb'
+- './spec/graphql/resolvers/project_pipeline_statistics_resolver_spec.rb'
+- './spec/graphql/resolvers/project_resolver_spec.rb'
+- './spec/graphql/resolvers/projects/fork_targets_resolver_spec.rb'
+- './spec/graphql/resolvers/projects/grafana_integration_resolver_spec.rb'
+- './spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb'
+- './spec/graphql/resolvers/projects_resolver_spec.rb'
+- './spec/graphql/resolvers/projects/services_resolver_spec.rb'
+- './spec/graphql/resolvers/projects/snippets_resolver_spec.rb'
+- './spec/graphql/resolvers/recent_boards_resolver_spec.rb'
+- './spec/graphql/resolvers/release_milestones_resolver_spec.rb'
+- './spec/graphql/resolvers/release_resolver_spec.rb'
+- './spec/graphql/resolvers/releases_resolver_spec.rb'
+- './spec/graphql/resolvers/repository_branch_names_resolver_spec.rb'
+- './spec/graphql/resolvers/snippets/blobs_resolver_spec.rb'
+- './spec/graphql/resolvers/snippets_resolver_spec.rb'
+- './spec/graphql/resolvers/terraform/states_resolver_spec.rb'
+- './spec/graphql/resolvers/timelog_resolver_spec.rb'
+- './spec/graphql/resolvers/todos_resolver_spec.rb'
+- './spec/graphql/resolvers/topics_resolver_spec.rb'
+- './spec/graphql/resolvers/tree_resolver_spec.rb'
+- './spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
+- './spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
+- './spec/graphql/resolvers/user_resolver_spec.rb'
+- './spec/graphql/resolvers/users/group_count_resolver_spec.rb'
+- './spec/graphql/resolvers/users/groups_resolver_spec.rb'
+- './spec/graphql/resolvers/users/participants_resolver_spec.rb'
+- './spec/graphql/resolvers/users_resolver_spec.rb'
+- './spec/graphql/resolvers/users/snippets_resolver_spec.rb'
+- './spec/graphql/resolvers/work_item_resolver_spec.rb'
+- './spec/graphql/resolvers/work_items_resolver_spec.rb'
+- './spec/graphql/resolvers/work_items/types_resolver_spec.rb'
+- './spec/graphql/subscriptions/issuable_updated_spec.rb'
+- './spec/graphql/types/access_level_enum_spec.rb'
+- './spec/graphql/types/access_level_type_spec.rb'
+- './spec/graphql/types/admin/analytics/usage_trends/measurement_identifier_enum_spec.rb'
+- './spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb'
+- './spec/graphql/types/alert_management/alert_status_count_type_spec.rb'
+- './spec/graphql/types/alert_management/alert_type_spec.rb'
+- './spec/graphql/types/alert_management/domain_filter_enum_spec.rb'
+- './spec/graphql/types/alert_management/http_integration_type_spec.rb'
+- './spec/graphql/types/alert_management/integration_type_enum_spec.rb'
+- './spec/graphql/types/alert_management/integration_type_spec.rb'
+- './spec/graphql/types/alert_management/prometheus_integration_type_spec.rb'
+- './spec/graphql/types/alert_management/severity_enum_spec.rb'
+- './spec/graphql/types/alert_management/status_enum_spec.rb'
+- './spec/graphql/types/availability_enum_spec.rb'
+- './spec/graphql/types/award_emojis/award_emoji_type_spec.rb'
+- './spec/graphql/types/base_argument_spec.rb'
+- './spec/graphql/types/base_edge_spec.rb'
+- './spec/graphql/types/base_enum_spec.rb'
+- './spec/graphql/types/base_field_spec.rb'
+- './spec/graphql/types/base_object_spec.rb'
+- './spec/graphql/types/blob_viewers/type_enum_spec.rb'
+- './spec/graphql/types/blob_viewer_type_spec.rb'
+- './spec/graphql/types/board_list_type_spec.rb'
+- './spec/graphql/types/boards/board_issue_input_type_spec.rb'
+- './spec/graphql/types/board_type_spec.rb'
+- './spec/graphql/types/branch_type_spec.rb'
+- './spec/graphql/types/ci/analytics_type_spec.rb'
+- './spec/graphql/types/ci/config/config_type_spec.rb'
+- './spec/graphql/types/ci/config/group_type_spec.rb'
+- './spec/graphql/types/ci/config/include_type_enum_spec.rb'
+- './spec/graphql/types/ci/config/include_type_spec.rb'
+- './spec/graphql/types/ci/config/job_restriction_type_spec.rb'
+- './spec/graphql/types/ci/config/job_type_spec.rb'
+- './spec/graphql/types/ci/config/need_type_spec.rb'
+- './spec/graphql/types/ci/config/stage_type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/analyzers_entity_input_type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/analyzers_entity_type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/entity_input_type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/entity_type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/input_type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/options_entity_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/type_spec.rb'
+- './spec/graphql/types/ci_configuration/sast/ui_component_size_enum_spec.rb'
+- './spec/graphql/types/ci/detailed_status_type_spec.rb'
+- './spec/graphql/types/ci/group_type_spec.rb'
+- './spec/graphql/types/ci/group_variable_type_spec.rb'
+- './spec/graphql/types/ci/instance_variable_type_spec.rb'
+- './spec/graphql/types/ci/job_artifact_file_type_enum_spec.rb'
+- './spec/graphql/types/ci/job_artifact_type_spec.rb'
+- './spec/graphql/types/ci/job_kind_enum_spec.rb'
+- './spec/graphql/types/ci/job_need_union_spec.rb'
+- './spec/graphql/types/ci/job_status_enum_spec.rb'
+- './spec/graphql/types/ci/job_token_scope_type_spec.rb'
+- './spec/graphql/types/ci/job_type_spec.rb'
+- './spec/graphql/types/ci/manual_variable_type_spec.rb'
+- './spec/graphql/types/ci/pipeline_counts_type_spec.rb'
+- './spec/graphql/types/ci/pipeline_merge_request_event_type_enum_spec.rb'
+- './spec/graphql/types/ci/pipeline_message_type_spec.rb'
+- './spec/graphql/types/ci/pipeline_scope_enum_spec.rb'
+- './spec/graphql/types/ci/pipeline_status_enum_spec.rb'
+- './spec/graphql/types/ci/pipeline_type_spec.rb'
+- './spec/graphql/types/ci/project_variable_type_spec.rb'
+- './spec/graphql/types/ci/recent_failures_type_spec.rb'
+- './spec/graphql/types/ci/runner_architecture_type_spec.rb'
+- './spec/graphql/types/ci/runner_platform_type_spec.rb'
+- './spec/graphql/types/ci/runner_setup_type_spec.rb'
+- './spec/graphql/types/ci/runner_type_spec.rb'
+- './spec/graphql/types/ci/runner_upgrade_status_enum_spec.rb'
+- './spec/graphql/types/ci/runner_web_url_edge_spec.rb'
+- './spec/graphql/types/ci/stage_type_spec.rb'
+- './spec/graphql/types/ci/status_action_type_spec.rb'
+- './spec/graphql/types/ci/template_type_spec.rb'
+- './spec/graphql/types/ci/test_case_status_enum_spec.rb'
+- './spec/graphql/types/ci/test_case_type_spec.rb'
+- './spec/graphql/types/ci/test_report_summary_type_spec.rb'
+- './spec/graphql/types/ci/test_report_total_type_spec.rb'
+- './spec/graphql/types/ci/test_suite_summary_type_spec.rb'
+- './spec/graphql/types/ci/test_suite_type_spec.rb'
+- './spec/graphql/types/ci/variable_input_type_spec.rb'
+- './spec/graphql/types/ci/variable_interface_spec.rb'
+- './spec/graphql/types/ci/variable_type_enum_spec.rb'
+- './spec/graphql/types/clusters/agent_activity_event_type_spec.rb'
+- './spec/graphql/types/clusters/agent_token_status_enum_spec.rb'
+- './spec/graphql/types/clusters/agent_token_type_spec.rb'
+- './spec/graphql/types/clusters/agent_type_spec.rb'
+- './spec/graphql/types/color_type_spec.rb'
+- './spec/graphql/types/commit_action_mode_enum_spec.rb'
+- './spec/graphql/types/commit_encoding_enum_spec.rb'
+- './spec/graphql/types/commit_type_spec.rb'
+- './spec/graphql/types/container_expiration_policy_cadence_enum_spec.rb'
+- './spec/graphql/types/container_expiration_policy_keep_enum_spec.rb'
+- './spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb'
+- './spec/graphql/types/container_expiration_policy_type_spec.rb'
+- './spec/graphql/types/container_repository_cleanup_status_enum_spec.rb'
+- './spec/graphql/types/container_repository_details_type_spec.rb'
+- './spec/graphql/types/container_repository_sort_enum_spec.rb'
+- './spec/graphql/types/container_repository_status_enum_spec.rb'
+- './spec/graphql/types/container_repository_tag_type_spec.rb'
+- './spec/graphql/types/container_repository_type_spec.rb'
+- './spec/graphql/types/container_respository_tags_sort_enum_spec.rb'
+- './spec/graphql/types/countable_connection_type_spec.rb'
+- './spec/graphql/types/current_user_todos_type_spec.rb'
+- './spec/graphql/types/custom_emoji_type_spec.rb'
+- './spec/graphql/types/customer_relations/contact_sort_enum_spec.rb'
+- './spec/graphql/types/customer_relations/contact_state_counts_type_spec.rb'
+- './spec/graphql/types/customer_relations/contact_type_spec.rb'
+- './spec/graphql/types/customer_relations/organization_sort_enum_spec.rb'
+- './spec/graphql/types/customer_relations/organization_state_counts_type_spec.rb'
+- './spec/graphql/types/customer_relations/organization_type_spec.rb'
+- './spec/graphql/types/dependency_proxy/blob_type_spec.rb'
+- './spec/graphql/types/dependency_proxy/group_setting_type_spec.rb'
+- './spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb'
+- './spec/graphql/types/dependency_proxy/manifest_type_spec.rb'
+- './spec/graphql/types/deployment_tier_enum_spec.rb'
+- './spec/graphql/types/design_management/design_at_version_type_spec.rb'
+- './spec/graphql/types/design_management/design_collection_copy_state_enum_spec.rb'
+- './spec/graphql/types/design_management/design_collection_type_spec.rb'
+- './spec/graphql/types/design_management/design_type_spec.rb'
+- './spec/graphql/types/design_management/design_version_event_enum_spec.rb'
+- './spec/graphql/types/design_management_type_spec.rb'
+- './spec/graphql/types/design_management/version_type_spec.rb'
+- './spec/graphql/types/diff_refs_type_spec.rb'
+- './spec/graphql/types/duration_type_spec.rb'
+- './spec/graphql/types/environment_type_spec.rb'
+- './spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb'
+- './spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb'
+- './spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb'
+- './spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb'
+- './spec/graphql/types/error_tracking/sentry_error_type_spec.rb'
+- './spec/graphql/types/eventable_type_spec.rb'
+- './spec/graphql/types/event_type_spec.rb'
+- './spec/graphql/types/evidence_type_spec.rb'
+- './spec/graphql/types/global_id_type_spec.rb'
+- './spec/graphql/types/grafana_integration_type_spec.rb'
+- './spec/graphql/types/group_invitation_type_spec.rb'
+- './spec/graphql/types/group_member_relation_enum_spec.rb'
+- './spec/graphql/types/group_member_type_spec.rb'
+- './spec/graphql/types/group_type_spec.rb'
+- './spec/graphql/types/incident_management/escalation_status_enum_spec.rb'
+- './spec/graphql/types/incident_management/timeline_event_type_spec.rb'
+- './spec/graphql/types/invitation_interface_spec.rb'
+- './spec/graphql/types/issuable_searchable_field_enum_spec.rb'
+- './spec/graphql/types/issuable_severity_enum_spec.rb'
+- './spec/graphql/types/issuable_sort_enum_spec.rb'
+- './spec/graphql/types/issuable_state_enum_spec.rb'
+- './spec/graphql/types/issuable_type_spec.rb'
+- './spec/graphql/types/issue_sort_enum_spec.rb'
+- './spec/graphql/types/issue_state_enum_spec.rb'
+- './spec/graphql/types/issue_status_count_type_spec.rb'
+- './spec/graphql/types/issue_type_enum_spec.rb'
+- './spec/graphql/types/issue_type_spec.rb'
+- './spec/graphql/types/jira_import_type_spec.rb'
+- './spec/graphql/types/jira_user_type_spec.rb'
+- './spec/graphql/types/kas/agent_configuration_type_spec.rb'
+- './spec/graphql/types/kas/agent_connection_type_spec.rb'
+- './spec/graphql/types/kas/agent_metadata_type_spec.rb'
+- './spec/graphql/types/label_type_spec.rb'
+- './spec/graphql/types/limited_countable_connection_type_spec.rb'
+- './spec/graphql/types/member_interface_spec.rb'
+- './spec/graphql/types/merge_request_connection_type_spec.rb'
+- './spec/graphql/types/merge_request_review_state_enum_spec.rb'
+- './spec/graphql/types/merge_requests/assignee_type_spec.rb'
+- './spec/graphql/types/merge_requests/author_type_spec.rb'
+- './spec/graphql/types/merge_request_sort_enum_spec.rb'
+- './spec/graphql/types/merge_requests/participant_type_spec.rb'
+- './spec/graphql/types/merge_requests/reviewer_type_spec.rb'
+- './spec/graphql/types/merge_request_state_enum_spec.rb'
+- './spec/graphql/types/merge_request_state_event_enum_spec.rb'
+- './spec/graphql/types/merge_request_type_spec.rb'
+- './spec/graphql/types/metadata/kas_type_spec.rb'
+- './spec/graphql/types/metadata_type_spec.rb'
+- './spec/graphql/types/metrics/dashboards/annotation_type_spec.rb'
+- './spec/graphql/types/metrics/dashboard_type_spec.rb'
+- './spec/graphql/types/milestone_stats_type_spec.rb'
+- './spec/graphql/types/milestone_type_spec.rb'
+- './spec/graphql/types/mutation_type_spec.rb'
+- './spec/graphql/types/namespace/package_settings_type_spec.rb'
+- './spec/graphql/types/namespace_type_spec.rb'
+- './spec/graphql/types/notes/diff_position_type_spec.rb'
+- './spec/graphql/types/notes/discussion_type_spec.rb'
+- './spec/graphql/types/notes/noteable_interface_spec.rb'
+- './spec/graphql/types/notes/note_type_spec.rb'
+- './spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb'
+- './spec/graphql/types/packages/cleanup/policy_type_spec.rb'
+- './spec/graphql/types/packages/composer/json_type_spec.rb'
+- './spec/graphql/types/packages/composer/metadatum_type_spec.rb'
+- './spec/graphql/types/packages/conan/file_metadatum_type_spec.rb'
+- './spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb'
+- './spec/graphql/types/packages/conan/metadatum_type_spec.rb'
+- './spec/graphql/types/packages/helm/dependency_type_spec.rb'
+- './spec/graphql/types/packages/helm/file_metadatum_type_spec.rb'
+- './spec/graphql/types/packages/helm/maintainer_type_spec.rb'
+- './spec/graphql/types/packages/helm/metadata_type_spec.rb'
+- './spec/graphql/types/packages/maven/metadatum_type_spec.rb'
+- './spec/graphql/types/packages/nuget/dependency_link_metdatum_type_spec.rb'
+- './spec/graphql/types/packages/nuget/metadatum_type_spec.rb'
+- './spec/graphql/types/packages/package_base_type_spec.rb'
+- './spec/graphql/types/packages/package_dependency_link_type_spec.rb'
+- './spec/graphql/types/packages/package_dependency_type_enum_spec.rb'
+- './spec/graphql/types/packages/package_dependency_type_spec.rb'
+- './spec/graphql/types/packages/package_details_type_spec.rb'
+- './spec/graphql/types/packages/package_file_type_spec.rb'
+- './spec/graphql/types/packages/package_group_sort_enum_spec.rb'
+- './spec/graphql/types/packages/package_sort_enum_spec.rb'
+- './spec/graphql/types/packages/package_status_enum_spec.rb'
+- './spec/graphql/types/packages/package_type_enum_spec.rb'
+- './spec/graphql/types/packages/package_type_spec.rb'
+- './spec/graphql/types/packages/pypi/metadatum_type_spec.rb'
+- './spec/graphql/types/packages/tag_type_spec.rb'
+- './spec/graphql/types/permission_types/base_permission_type_spec.rb'
+- './spec/graphql/types/permission_types/ci/job_spec.rb'
+- './spec/graphql/types/permission_types/ci/runner_spec.rb'
+- './spec/graphql/types/permission_types/issue_spec.rb'
+- './spec/graphql/types/permission_types/merge_request_spec.rb'
+- './spec/graphql/types/permission_types/merge_request_type_spec.rb'
+- './spec/graphql/types/permission_types/note_spec.rb'
+- './spec/graphql/types/permission_types/project_spec.rb'
+- './spec/graphql/types/permission_types/snippet_spec.rb'
+- './spec/graphql/types/permission_types/user_spec.rb'
+- './spec/graphql/types/permission_types/work_item_spec.rb'
+- './spec/graphql/types/project_invitation_type_spec.rb'
+- './spec/graphql/types/project_member_relation_enum_spec.rb'
+- './spec/graphql/types/project_member_type_spec.rb'
+- './spec/graphql/types/projects/base_service_type_spec.rb'
+- './spec/graphql/types/projects/jira_project_type_spec.rb'
+- './spec/graphql/types/projects/jira_service_type_spec.rb'
+- './spec/graphql/types/projects/service_type_enum_spec.rb'
+- './spec/graphql/types/projects/service_type_spec.rb'
+- './spec/graphql/types/project_statistics_type_spec.rb'
+- './spec/graphql/types/projects/topic_type_spec.rb'
+- './spec/graphql/types/project_type_spec.rb'
+- './spec/graphql/types/prometheus_alert_type_spec.rb'
+- './spec/graphql/types/query_complexity_type_spec.rb'
+- './spec/graphql/types/query_type_spec.rb'
+- './spec/graphql/types/range_input_type_spec.rb'
+- './spec/graphql/types/release_asset_link_input_type_spec.rb'
+- './spec/graphql/types/release_asset_link_type_spec.rb'
+- './spec/graphql/types/release_assets_input_type_spec.rb'
+- './spec/graphql/types/release_assets_type_spec.rb'
+- './spec/graphql/types/release_links_type_spec.rb'
+- './spec/graphql/types/release_source_type_spec.rb'
+- './spec/graphql/types/release_type_spec.rb'
+- './spec/graphql/types/repository/blob_type_spec.rb'
+- './spec/graphql/types/repository_type_spec.rb'
+- './spec/graphql/types/resolvable_interface_spec.rb'
+- './spec/graphql/types/root_storage_statistics_type_spec.rb'
+- './spec/graphql/types/saved_reply_type_spec.rb'
+- './spec/graphql/types/security/report_types_enum_spec.rb'
+- './spec/graphql/types/snippets/blob_action_enum_spec.rb'
+- './spec/graphql/types/snippets/blob_action_input_type_spec.rb'
+- './spec/graphql/types/snippets/blob_type_spec.rb'
+- './spec/graphql/types/snippets/blob_viewer_type_spec.rb'
+- './spec/graphql/types/snippet_type_spec.rb'
+- './spec/graphql/types/subscription_type_spec.rb'
+- './spec/graphql/types/terraform/state_type_spec.rb'
+- './spec/graphql/types/terraform/state_version_type_spec.rb'
+- './spec/graphql/types/timeframe_type_spec.rb'
+- './spec/graphql/types/timelog_type_spec.rb'
+- './spec/graphql/types/time_tracking/timelog_category_type_spec.rb'
+- './spec/graphql/types/time_type_spec.rb'
+- './spec/graphql/types/todoable_interface_spec.rb'
+- './spec/graphql/types/todo_type_spec.rb'
+- './spec/graphql/types/tree/blob_type_spec.rb'
+- './spec/graphql/types/tree/submodule_type_spec.rb'
+- './spec/graphql/types/tree/tree_entry_type_spec.rb'
+- './spec/graphql/types/tree/tree_type_spec.rb'
+- './spec/graphql/types/tree/type_enum_spec.rb'
+- './spec/graphql/types/untrusted_regexp_spec.rb'
+- './spec/graphql/types/upload_type_spec.rb'
+- './spec/graphql/types/user_callout_feature_name_enum_spec.rb'
+- './spec/graphql/types/user_callout_type_spec.rb'
+- './spec/graphql/types/user_merge_request_interaction_type_spec.rb'
+- './spec/graphql/types/user_preferences_type_spec.rb'
+- './spec/graphql/types/user_status_type_spec.rb'
+- './spec/graphql/types/user_type_spec.rb'
+- './spec/graphql/types/work_item_id_type_spec.rb'
+- './spec/graphql/types/work_items/widget_interface_spec.rb'
+- './spec/graphql/types/work_items/widgets/assignees_input_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/assignees_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/description_input_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/description_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/hierarchy_update_input_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/labels_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/start_and_due_date_type_spec.rb'
+- './spec/graphql/types/work_items/widgets/start_and_due_date_update_input_type_spec.rb'
+- './spec/graphql/types/work_items/widget_type_enum_spec.rb'
+- './spec/graphql/types/work_item_type_spec.rb'
+- './spec/haml_lint/linter/documentation_links_spec.rb'
+- './spec/haml_lint/linter/inline_javascript_spec.rb'
+- './spec/haml_lint/linter/no_plain_nodes_spec.rb'
+- './spec/helpers/access_tokens_helper_spec.rb'
+- './spec/helpers/admin/application_settings/settings_helper_spec.rb'
+- './spec/helpers/admin/background_migrations_helper_spec.rb'
+- './spec/helpers/admin/deploy_key_helper_spec.rb'
+- './spec/helpers/admin/identities_helper_spec.rb'
+- './spec/helpers/admin/user_actions_helper_spec.rb'
+- './spec/helpers/analytics/cycle_analytics_helper_spec.rb'
+- './spec/helpers/appearances_helper_spec.rb'
+- './spec/helpers/application_helper_spec.rb'
+- './spec/helpers/application_settings_helper_spec.rb'
+- './spec/helpers/auth_helper_spec.rb'
+- './spec/helpers/auto_devops_helper_spec.rb'
+- './spec/helpers/avatars_helper_spec.rb'
+- './spec/helpers/award_emoji_helper_spec.rb'
+- './spec/helpers/badges_helper_spec.rb'
+- './spec/helpers/bizible_helper_spec.rb'
+- './spec/helpers/blame_helper_spec.rb'
+- './spec/helpers/blob_helper_spec.rb'
+- './spec/helpers/boards_helper_spec.rb'
+- './spec/helpers/branches_helper_spec.rb'
+- './spec/helpers/breadcrumbs_helper_spec.rb'
+- './spec/helpers/broadcast_messages_helper_spec.rb'
+- './spec/helpers/button_helper_spec.rb'
+- './spec/helpers/calendar_helper_spec.rb'
+- './spec/helpers/ci/builds_helper_spec.rb'
+- './spec/helpers/ci/jobs_helper_spec.rb'
+- './spec/helpers/ci/pipeline_editor_helper_spec.rb'
+- './spec/helpers/ci/pipelines_helper_spec.rb'
+- './spec/helpers/ci/runners_helper_spec.rb'
+- './spec/helpers/ci/secure_files_helper_spec.rb'
+- './spec/helpers/ci/status_helper_spec.rb'
+- './spec/helpers/ci/triggers_helper_spec.rb'
+- './spec/helpers/clusters_helper_spec.rb'
+- './spec/helpers/colors_helper_spec.rb'
+- './spec/helpers/commits_helper_spec.rb'
+- './spec/helpers/components_helper_spec.rb'
+- './spec/helpers/container_expiration_policies_helper_spec.rb'
+- './spec/helpers/container_registry_helper_spec.rb'
+- './spec/helpers/cookies_helper_spec.rb'
+- './spec/helpers/dashboard_helper_spec.rb'
+- './spec/helpers/deploy_tokens_helper_spec.rb'
+- './spec/helpers/dev_ops_report_helper_spec.rb'
+- './spec/helpers/diff_helper_spec.rb'
+- './spec/helpers/dropdowns_helper_spec.rb'
+- './spec/helpers/emails_helper_spec.rb'
+- './spec/helpers/emoji_helper_spec.rb'
+- './spec/helpers/enable_search_settings_helper_spec.rb'
+- './spec/helpers/environment_helper_spec.rb'
+- './spec/helpers/environments_helper_spec.rb'
+- './spec/helpers/events_helper_spec.rb'
+- './spec/helpers/explore_helper_spec.rb'
+- './spec/helpers/export_helper_spec.rb'
+- './spec/helpers/external_link_helper_spec.rb'
+- './spec/helpers/feature_flags_helper_spec.rb'
+- './spec/helpers/form_helper_spec.rb'
+- './spec/helpers/git_helper_spec.rb'
+- './spec/helpers/gitlab_routing_helper_spec.rb'
+- './spec/helpers/gitlab_script_tag_helper_spec.rb'
+- './spec/helpers/graph_helper_spec.rb'
+- './spec/helpers/groups/group_members_helper_spec.rb'
+- './spec/helpers/groups_helper_spec.rb'
+- './spec/helpers/groups/settings_helper_spec.rb'
+- './spec/helpers/hooks_helper_spec.rb'
+- './spec/helpers/icons_helper_spec.rb'
+- './spec/helpers/ide_helper_spec.rb'
+- './spec/helpers/import_helper_spec.rb'
+- './spec/helpers/instance_configuration_helper_spec.rb'
+- './spec/helpers/integrations_helper_spec.rb'
+- './spec/helpers/invite_members_helper_spec.rb'
+- './spec/helpers/issuables_description_templates_helper_spec.rb'
+- './spec/helpers/issuables_helper_spec.rb'
+- './spec/helpers/issues_helper_spec.rb'
+- './spec/helpers/jira_connect_helper_spec.rb'
+- './spec/helpers/keyset_helper_spec.rb'
+- './spec/helpers/labels_helper_spec.rb'
+- './spec/helpers/lazy_image_tag_helper_spec.rb'
+- './spec/helpers/listbox_helper_spec.rb'
+- './spec/helpers/markup_helper_spec.rb'
+- './spec/helpers/members_helper_spec.rb'
+- './spec/helpers/merge_requests_helper_spec.rb'
+- './spec/helpers/namespaces_helper_spec.rb'
+- './spec/helpers/nav_helper_spec.rb'
+- './spec/helpers/nav/new_dropdown_helper_spec.rb'
+- './spec/helpers/nav/top_nav_helper_spec.rb'
+- './spec/helpers/notes_helper_spec.rb'
+- './spec/helpers/notifications_helper_spec.rb'
+- './spec/helpers/notify_helper_spec.rb'
+- './spec/helpers/numbers_helper_spec.rb'
+- './spec/helpers/one_trust_helper_spec.rb'
+- './spec/helpers/operations_helper_spec.rb'
+- './spec/helpers/packages_helper_spec.rb'
+- './spec/helpers/page_layout_helper_spec.rb'
+- './spec/helpers/pagination_helper_spec.rb'
+- './spec/helpers/preferences_helper_spec.rb'
+- './spec/helpers/profiles_helper_spec.rb'
+- './spec/helpers/projects/alert_management_helper_spec.rb'
+- './spec/helpers/projects/cluster_agents_helper_spec.rb'
+- './spec/helpers/projects/error_tracking_helper_spec.rb'
+- './spec/helpers/projects_helper_spec.rb'
+- './spec/helpers/projects/incidents_helper_spec.rb'
+- './spec/helpers/projects/pipeline_helper_spec.rb'
+- './spec/helpers/projects/project_members_helper_spec.rb'
+- './spec/helpers/projects/security/configuration_helper_spec.rb'
+- './spec/helpers/projects/terraform_helper_spec.rb'
+- './spec/helpers/recaptcha_helper_spec.rb'
+- './spec/helpers/registrations_helper_spec.rb'
+- './spec/helpers/releases_helper_spec.rb'
+- './spec/helpers/routing/pseudonymization_helper_spec.rb'
+- './spec/helpers/rss_helper_spec.rb'
+- './spec/helpers/search_helper_spec.rb'
+- './spec/helpers/sessions_helper_spec.rb'
+- './spec/helpers/sidebars_helper_spec.rb'
+- './spec/helpers/sidekiq_helper_spec.rb'
+- './spec/helpers/snippets_helper_spec.rb'
+- './spec/helpers/sorting_helper_spec.rb'
+- './spec/helpers/sourcegraph_helper_spec.rb'
+- './spec/helpers/ssh_keys_helper_spec.rb'
+- './spec/helpers/startupjs_helper_spec.rb'
+- './spec/helpers/stat_anchors_helper_spec.rb'
+- './spec/helpers/storage_helper_spec.rb'
+- './spec/helpers/submodule_helper_spec.rb'
+- './spec/helpers/subscribable_banner_helper_spec.rb'
+- './spec/helpers/tab_helper_spec.rb'
+- './spec/helpers/terms_helper_spec.rb'
+- './spec/helpers/timeboxes_helper_spec.rb'
+- './spec/helpers/timeboxes_routing_helper_spec.rb'
+- './spec/helpers/time_helper_spec.rb'
+- './spec/helpers/time_zone_helper_spec.rb'
+- './spec/helpers/todos_helper_spec.rb'
+- './spec/helpers/tooling/visual_review_helper_spec.rb'
+- './spec/helpers/tracking_helper_spec.rb'
+- './spec/helpers/tree_helper_spec.rb'
+- './spec/helpers/users/callouts_helper_spec.rb'
+- './spec/helpers/users/group_callouts_helper_spec.rb'
+- './spec/helpers/users_helper_spec.rb'
+- './spec/helpers/version_check_helper_spec.rb'
+- './spec/helpers/visibility_level_helper_spec.rb'
+- './spec/helpers/web_hooks/web_hooks_helper_spec.rb'
+- './spec/helpers/web_ide_button_helper_spec.rb'
+- './spec/helpers/webpack_helper_spec.rb'
+- './spec/helpers/whats_new_helper_spec.rb'
+- './spec/helpers/wiki_helper_spec.rb'
+- './spec/helpers/wiki_page_version_helper_spec.rb'
+- './spec/helpers/x509_helper_spec.rb'
+- './spec/initializers/00_deprecations_spec.rb'
+- './spec/initializers/00_rails_disable_joins_spec.rb'
+- './spec/initializers/0_log_deprecations_spec.rb'
+- './spec/initializers/0_postgresql_types_spec.rb'
+- './spec/initializers/100_patch_omniauth_oauth2_spec.rb'
+- './spec/initializers/100_patch_omniauth_saml_spec.rb'
+- './spec/initializers/1_acts_as_taggable_spec.rb'
+- './spec/initializers/6_validations_spec.rb'
+- './spec/initializers/action_cable_subscription_adapter_identifier_spec.rb'
+- './spec/initializers/action_mailer_hooks_spec.rb'
+- './spec/initializers/active_record_locking_spec.rb'
+- './spec/initializers/asset_proxy_setting_spec.rb'
+- './spec/initializers/attr_encrypted_no_db_connection_spec.rb'
+- './spec/initializers/attr_encrypted_thread_safe_spec.rb'
+- './spec/initializers/carrierwave_patch_spec.rb'
+- './spec/initializers/cookies_serializer_spec.rb'
+- './spec/initializers/database_config_spec.rb'
+- './spec/initializers/diagnostic_reports_spec.rb'
+- './spec/initializers/direct_upload_support_spec.rb'
+- './spec/initializers/doorkeeper_spec.rb'
+- './spec/initializers/enumerator_next_patch_spec.rb'
+- './spec/initializers/fog_google_https_private_urls_spec.rb'
+- './spec/initializers/forbid_sidekiq_in_transactions_spec.rb'
+- './spec/initializers/global_id_spec.rb'
+- './spec/initializers/google_api_client_spec.rb'
+- './spec/initializers/hangouts_chat_http_override_spec.rb'
+- './spec/initializers/lograge_spec.rb'
+- './spec/initializers/mail_encoding_patch_spec.rb'
+- './spec/initializers/mailer_retries_spec.rb'
+- './spec/initializers/memory_watchdog_spec.rb'
+- './spec/initializers/net_http_patch_spec.rb'
+- './spec/initializers/net_http_response_patch_spec.rb'
+- './spec/initializers/omniauth_spec.rb'
+- './spec/initializers/pages_storage_check_spec.rb'
+- './spec/initializers/rack_multipart_patch_spec.rb'
+- './spec/initializers/rails_asset_host_spec.rb'
+- './spec/initializers/rdoc_segfault_patch_spec.rb'
+- './spec/initializers/remove_active_job_execute_callback_spec.rb'
+- './spec/initializers/rest-client-hostname_override_spec.rb'
+- './spec/initializers/secret_token_spec.rb'
+- './spec/initializers/session_store_spec.rb'
+- './spec/initializers/settings_spec.rb'
+- './spec/initializers/sidekiq_spec.rb'
+- './spec/initializers/trusted_proxies_spec.rb'
+- './spec/initializers/validate_database_config_spec.rb'
+- './spec/initializers/validate_puma_spec.rb'
+- './spec/lib/api/api_spec.rb'
+- './spec/lib/api/base_spec.rb'
+- './spec/lib/api/ci/helpers/runner_helpers_spec.rb'
+- './spec/lib/api/ci/helpers/runner_spec.rb'
+- './spec/lib/api/entities/application_setting_spec.rb'
+- './spec/lib/api/entities/basic_project_details_spec.rb'
+- './spec/lib/api/entities/branch_spec.rb'
+- './spec/lib/api/entities/bulk_imports/entity_failure_spec.rb'
+- './spec/lib/api/entities/bulk_imports/entity_spec.rb'
+- './spec/lib/api/entities/bulk_imports/export_status_spec.rb'
+- './spec/lib/api/entities/bulk_import_spec.rb'
+- './spec/lib/api/entities/changelog_spec.rb'
+- './spec/lib/api/entities/ci/job_artifact_file_spec.rb'
+- './spec/lib/api/entities/ci/job_request/dependency_spec.rb'
+- './spec/lib/api/entities/ci/job_request/image_spec.rb'
+- './spec/lib/api/entities/ci/job_request/port_spec.rb'
+- './spec/lib/api/entities/ci/job_request/service_spec.rb'
+- './spec/lib/api/entities/ci/pipeline_spec.rb'
+- './spec/lib/api/entities/clusters/agent_authorization_spec.rb'
+- './spec/lib/api/entities/clusters/agent_spec.rb'
+- './spec/lib/api/entities/deploy_key_spec.rb'
+- './spec/lib/api/entities/deploy_keys_project_spec.rb'
+- './spec/lib/api/entities/deployment_extended_spec.rb'
+- './spec/lib/api/entities/design_management/design_spec.rb'
+- './spec/lib/api/entities/group_detail_spec.rb'
+- './spec/lib/api/entities/merge_request_approvals_spec.rb'
+- './spec/lib/api/entities/merge_request_basic_spec.rb'
+- './spec/lib/api/entities/merge_request_changes_spec.rb'
+- './spec/lib/api/entities/nuget/dependency_group_spec.rb'
+- './spec/lib/api/entities/nuget/dependency_spec.rb'
+- './spec/lib/api/entities/nuget/metadatum_spec.rb'
+- './spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb'
+- './spec/lib/api/entities/nuget/search_result_spec.rb'
+- './spec/lib/api/entities/package_spec.rb'
+- './spec/lib/api/entities/personal_access_token_spec.rb'
+- './spec/lib/api/entities/personal_access_token_with_details_spec.rb'
+- './spec/lib/api/entities/plan_limit_spec.rb'
+- './spec/lib/api/entities/project_import_failed_relation_spec.rb'
+- './spec/lib/api/entities/project_import_status_spec.rb'
+- './spec/lib/api/entities/project_spec.rb'
+- './spec/lib/api/entities/projects/repository_storage_move_spec.rb'
+- './spec/lib/api/entities/projects/topic_spec.rb'
+- './spec/lib/api/entities/public_group_details_spec.rb'
+- './spec/lib/api/entities/release_spec.rb'
+- './spec/lib/api/entities/snippet_spec.rb'
+- './spec/lib/api/entities/snippets/repository_storage_move_spec.rb'
+- './spec/lib/api/entities/ssh_key_spec.rb'
+- './spec/lib/api/entities/user_spec.rb'
+- './spec/lib/api/entities/wiki_page_spec.rb'
+- './spec/lib/api/every_api_endpoint_spec.rb'
+- './spec/lib/api/github/entities_spec.rb'
+- './spec/lib/api/helpers/authentication_spec.rb'
+- './spec/lib/api/helpers/caching_spec.rb'
+- './spec/lib/api/helpers/common_helpers_spec.rb'
+- './spec/lib/api/helpers/graphql_helpers_spec.rb'
+- './spec/lib/api/helpers/label_helpers_spec.rb'
+- './spec/lib/api/helpers/merge_requests_helpers_spec.rb'
+- './spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb'
+- './spec/lib/api/helpers/packages_helpers_spec.rb'
+- './spec/lib/api/helpers/packages_manager_clients_helpers_spec.rb'
+- './spec/lib/api/helpers/pagination_spec.rb'
+- './spec/lib/api/helpers/pagination_strategies_spec.rb'
+- './spec/lib/api/helpers/project_stats_refresh_conflicts_helpers_spec.rb'
+- './spec/lib/api/helpers/rate_limiter_spec.rb'
+- './spec/lib/api/helpers/related_resources_helpers_spec.rb'
+- './spec/lib/api/helpers_spec.rb'
+- './spec/lib/api/helpers/variables_helpers_spec.rb'
+- './spec/lib/api/helpers/version_spec.rb'
+- './spec/lib/api/integrations/slack/events/url_verification_spec.rb'
+- './spec/lib/api/support/git_access_actor_spec.rb'
+- './spec/lib/api/validations/validators/absence_spec.rb'
+- './spec/lib/api/validations/validators/array_none_any_spec.rb'
+- './spec/lib/api/validations/validators/email_or_email_list_spec.rb'
+- './spec/lib/api/validations/validators/file_path_spec.rb'
+- './spec/lib/api/validations/validators/git_ref_spec.rb'
+- './spec/lib/api/validations/validators/git_sha_spec.rb'
+- './spec/lib/api/validations/validators/integer_none_any_spec.rb'
+- './spec/lib/api/validations/validators/integer_or_custom_value_spec.rb'
+- './spec/lib/api/validations/validators/limit_spec.rb'
+- './spec/lib/api/validations/validators/project_portable_spec.rb'
+- './spec/lib/api/validations/validators/untrusted_regexp_spec.rb'
+- './spec/lib/atlassian/jira_connect/client_spec.rb'
+- './spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb'
+- './spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb'
+- './spec/lib/atlassian/jira_connect_spec.rb'
+- './spec/lib/atlassian/jira_issue_key_extractor_spec.rb'
+- './spec/lib/backup/database_backup_error_spec.rb'
+- './spec/lib/backup/database_spec.rb'
+- './spec/lib/backup/file_backup_error_spec.rb'
+- './spec/lib/backup/files_spec.rb'
+- './spec/lib/backup/gitaly_backup_spec.rb'
+- './spec/lib/backup/manager_spec.rb'
+- './spec/lib/backup/repositories_spec.rb'
+- './spec/lib/backup/task_spec.rb'
+- './spec/lib/banzai/color_parser_spec.rb'
+- './spec/lib/banzai/commit_renderer_spec.rb'
+- './spec/lib/banzai/cross_project_reference_spec.rb'
+- './spec/lib/banzai/filter/absolute_link_filter_spec.rb'
+- './spec/lib/banzai/filter_array_spec.rb'
+- './spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb'
+- './spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb'
+- './spec/lib/banzai/filter/asset_proxy_filter_spec.rb'
+- './spec/lib/banzai/filter/audio_link_filter_spec.rb'
+- './spec/lib/banzai/filter/autolink_filter_spec.rb'
+- './spec/lib/banzai/filter/blockquote_fence_filter_spec.rb'
+- './spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb'
+- './spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb'
+- './spec/lib/banzai/filter/color_filter_spec.rb'
+- './spec/lib/banzai/filter/commit_trailers_filter_spec.rb'
+- './spec/lib/banzai/filter/custom_emoji_filter_spec.rb'
+- './spec/lib/banzai/filter/emoji_filter_spec.rb'
+- './spec/lib/banzai/filter/external_link_filter_spec.rb'
+- './spec/lib/banzai/filter/footnote_filter_spec.rb'
+- './spec/lib/banzai/filter/front_matter_filter_spec.rb'
+- './spec/lib/banzai/filter/gollum_tags_filter_spec.rb'
+- './spec/lib/banzai/filter/html_entity_filter_spec.rb'
+- './spec/lib/banzai/filter/image_lazy_load_filter_spec.rb'
+- './spec/lib/banzai/filter/image_link_filter_spec.rb'
+- './spec/lib/banzai/filter/inline_alert_metrics_filter_spec.rb'
+- './spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb'
+- './spec/lib/banzai/filter/inline_diff_filter_spec.rb'
+- './spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb'
+- './spec/lib/banzai/filter/inline_metrics_filter_spec.rb'
+- './spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb'
+- './spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb'
+- './spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb'
+- './spec/lib/banzai/filter/kroki_filter_spec.rb'
+- './spec/lib/banzai/filter/markdown_filter_spec.rb'
+- './spec/lib/banzai/filter/math_filter_spec.rb'
+- './spec/lib/banzai/filter/mermaid_filter_spec.rb'
+- './spec/lib/banzai/filter/normalize_source_filter_spec.rb'
+- './spec/lib/banzai/filter/output_safety_spec.rb'
+- './spec/lib/banzai/filter/plantuml_filter_spec.rb'
+- './spec/lib/banzai/filter/reference_redactor_filter_spec.rb'
+- './spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/alert_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/commit_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/design_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/issue_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/label_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/project_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/reference_cache_spec.rb'
+- './spec/lib/banzai/filter/references/reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/references/user_reference_filter_spec.rb'
+- './spec/lib/banzai/filter/repository_link_filter_spec.rb'
+- './spec/lib/banzai/filter/sanitization_filter_spec.rb'
+- './spec/lib/banzai/filter/spaced_link_filter_spec.rb'
+- './spec/lib/banzai/filter/suggestion_filter_spec.rb'
+- './spec/lib/banzai/filter/syntax_highlight_filter_spec.rb'
+- './spec/lib/banzai/filter/table_of_contents_filter_spec.rb'
+- './spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb'
+- './spec/lib/banzai/filter/task_list_filter_spec.rb'
+- './spec/lib/banzai/filter/truncate_source_filter_spec.rb'
+- './spec/lib/banzai/filter/upload_link_filter_spec.rb'
+- './spec/lib/banzai/filter/video_link_filter_spec.rb'
+- './spec/lib/banzai/filter/wiki_link_filter_spec.rb'
+- './spec/lib/banzai/issuable_extractor_spec.rb'
+- './spec/lib/banzai/object_renderer_spec.rb'
+- './spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/description_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/email_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/emoji_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/full_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/gfm_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/post_process_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb'
+- './spec/lib/banzai/pipeline_spec.rb'
+- './spec/lib/banzai/pipeline/wiki_pipeline_spec.rb'
+- './spec/lib/banzai/querying_spec.rb'
+- './spec/lib/banzai/reference_parser/alert_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/base_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/commit_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/commit_range_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/design_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/external_issue_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/issue_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/label_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/merge_request_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/milestone_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/project_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/snippet_parser_spec.rb'
+- './spec/lib/banzai/reference_parser/user_parser_spec.rb'
+- './spec/lib/banzai/reference_redactor_spec.rb'
+- './spec/lib/banzai/render_context_spec.rb'
+- './spec/lib/banzai/renderer_spec.rb'
+- './spec/lib/bitbucket/collection_spec.rb'
+- './spec/lib/bitbucket/connection_spec.rb'
+- './spec/lib/bitbucket/page_spec.rb'
+- './spec/lib/bitbucket/paginator_spec.rb'
+- './spec/lib/bitbucket/representation/comment_spec.rb'
+- './spec/lib/bitbucket/representation/issue_spec.rb'
+- './spec/lib/bitbucket/representation/pull_request_comment_spec.rb'
+- './spec/lib/bitbucket/representation/pull_request_spec.rb'
+- './spec/lib/bitbucket/representation/repo_spec.rb'
+- './spec/lib/bitbucket/representation/user_spec.rb'
+- './spec/lib/bitbucket_server/client_spec.rb'
+- './spec/lib/bitbucket_server/collection_spec.rb'
+- './spec/lib/bitbucket_server/connection_spec.rb'
+- './spec/lib/bitbucket_server/page_spec.rb'
+- './spec/lib/bitbucket_server/paginator_spec.rb'
+- './spec/lib/bitbucket_server/representation/activity_spec.rb'
+- './spec/lib/bitbucket_server/representation/comment_spec.rb'
+- './spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb'
+- './spec/lib/bitbucket_server/representation/pull_request_spec.rb'
+- './spec/lib/bitbucket_server/representation/repo_spec.rb'
+- './spec/lib/bulk_imports/clients/graphql_spec.rb'
+- './spec/lib/bulk_imports/clients/http_spec.rb'
+- './spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb'
+- './spec/lib/bulk_imports/common/extractors/json_extractor_spec.rb'
+- './spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb'
+- './spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb'
+- './spec/lib/bulk_imports/common/graphql/get_members_query_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb'
+- './spec/lib/bulk_imports/common/rest/get_badges_query_spec.rb'
+- './spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb'
+- './spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb'
+- './spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb'
+- './spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb'
+- './spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb'
+- './spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb'
+- './spec/lib/bulk_imports/groups/pipelines/group_attributes_pipeline_spec.rb'
+- './spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb'
+- './spec/lib/bulk_imports/groups/pipelines/namespace_settings_pipeline_spec.rb'
+- './spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb'
+- './spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb'
+- './spec/lib/bulk_imports/groups/stage_spec.rb'
+- './spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb'
+- './spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb'
+- './spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb'
+- './spec/lib/bulk_imports/ndjson_pipeline_spec.rb'
+- './spec/lib/bulk_imports/network_error_spec.rb'
+- './spec/lib/bulk_imports/pipeline/context_spec.rb'
+- './spec/lib/bulk_imports/pipeline/extracted_data_spec.rb'
+- './spec/lib/bulk_imports/pipeline/runner_spec.rb'
+- './spec/lib/bulk_imports/pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb'
+- './spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb'
+- './spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/project_feature_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/service_desk_setting_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb'
+- './spec/lib/bulk_imports/projects/stage_spec.rb'
+- './spec/lib/bulk_imports/projects/transformers/project_attributes_transformer_spec.rb'
+- './spec/lib/bulk_imports/retry_pipeline_error_spec.rb'
+- './spec/lib/bulk_imports/users_mapper_spec.rb'
+- './spec/lib/constraints/admin_constrainer_spec.rb'
+- './spec/lib/constraints/group_url_constrainer_spec.rb'
+- './spec/lib/constraints/jira_encoded_url_constrainer_spec.rb'
+- './spec/lib/constraints/project_url_constrainer_spec.rb'
+- './spec/lib/constraints/user_url_constrainer_spec.rb'
+- './spec/lib/container_registry/blob_spec.rb'
+- './spec/lib/container_registry/client_spec.rb'
+- './spec/lib/container_registry/gitlab_api_client_spec.rb'
+- './spec/lib/container_registry/migration_spec.rb'
+- './spec/lib/container_registry/path_spec.rb'
+- './spec/lib/container_registry/registry_spec.rb'
+- './spec/lib/container_registry/tag_spec.rb'
+- './spec/lib/csv_builder_spec.rb'
+- './spec/lib/csv_builders/stream_spec.rb'
+- './spec/lib/declarative_enum_spec.rb'
+- './spec/lib/error_tracking/collector/payload_validator_spec.rb'
+- './spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb'
+- './spec/lib/error_tracking/collector/sentry_request_parser_spec.rb'
+- './spec/lib/error_tracking/sentry_client/api_urls_spec.rb'
+- './spec/lib/error_tracking/sentry_client/event_spec.rb'
+- './spec/lib/error_tracking/sentry_client/issue_link_spec.rb'
+- './spec/lib/error_tracking/sentry_client/issue_spec.rb'
+- './spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb'
+- './spec/lib/error_tracking/sentry_client/projects_spec.rb'
+- './spec/lib/error_tracking/sentry_client/repo_spec.rb'
+- './spec/lib/error_tracking/sentry_client_spec.rb'
+- './spec/lib/error_tracking/stacktrace_builder_spec.rb'
+- './spec/lib/event_filter_spec.rb'
+- './spec/lib/expand_variables_spec.rb'
+- './spec/lib/extracts_path_spec.rb'
+- './spec/lib/extracts_ref_spec.rb'
+- './spec/lib/feature/definition_spec.rb'
+- './spec/lib/feature/gitaly_spec.rb'
+- './spec/lib/feature_spec.rb'
+- './spec/lib/file_size_validator_spec.rb'
+- './spec/lib/forever_spec.rb'
+- './spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb'
+- './spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb'
+- './spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb'
+- './spec/lib/generators/gitlab/usage_metric_generator_spec.rb'
+- './spec/lib/generators/model/model_generator_spec.rb'
+- './spec/lib/gitaly/server_spec.rb'
+- './spec/lib/gitlab/access/branch_protection_spec.rb'
+- './spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb'
+- './spec/lib/gitlab/alert_management/alert_status_counts_spec.rb'
+- './spec/lib/gitlab/alert_management/fingerprint_spec.rb'
+- './spec/lib/gitlab/alert_management/payload/base_spec.rb'
+- './spec/lib/gitlab/alert_management/payload/generic_spec.rb'
+- './spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb'
+- './spec/lib/gitlab/alert_management/payload/prometheus_spec.rb'
+- './spec/lib/gitlab/alert_management/payload_spec.rb'
+- './spec/lib/gitlab/allowable_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_deployed_to_production_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb'
+- './spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb'
+- './spec/lib/gitlab/analytics/usage_trends/workers_argument_builder_spec.rb'
+- './spec/lib/gitlab/anonymous_session_spec.rb'
+- './spec/lib/gitlab/api_authentication/builder_spec.rb'
+- './spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb'
+- './spec/lib/gitlab/api_authentication/token_locator_spec.rb'
+- './spec/lib/gitlab/api_authentication/token_resolver_spec.rb'
+- './spec/lib/gitlab/api_authentication/token_type_builder_spec.rb'
+- './spec/lib/gitlab/app_json_logger_spec.rb'
+- './spec/lib/gitlab/application_context_spec.rb'
+- './spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb'
+- './spec/lib/gitlab/application_rate_limiter/increment_per_actioned_resource_spec.rb'
+- './spec/lib/gitlab/application_rate_limiter/increment_per_action_spec.rb'
+- './spec/lib/gitlab/application_rate_limiter_spec.rb'
+- './spec/lib/gitlab/app_logger_spec.rb'
+- './spec/lib/gitlab/app_text_logger_spec.rb'
+- './spec/lib/gitlab/asciidoc/html5_converter_spec.rb'
+- './spec/lib/gitlab/asciidoc/include_processor_spec.rb'
+- './spec/lib/gitlab/asciidoc_spec.rb'
+- './spec/lib/gitlab/asset_proxy_spec.rb'
+- './spec/lib/gitlab/audit/auditor_spec.rb'
+- './spec/lib/gitlab/audit/ci_runner_token_author_spec.rb'
+- './spec/lib/gitlab/audit/deploy_key_author_spec.rb'
+- './spec/lib/gitlab/audit/deploy_token_author_spec.rb'
+- './spec/lib/gitlab/audit/null_author_spec.rb'
+- './spec/lib/gitlab/audit/null_target_spec.rb'
+- './spec/lib/gitlab/audit/target_spec.rb'
+- './spec/lib/gitlab/audit/unauthenticated_author_spec.rb'
+- './spec/lib/gitlab/auth/activity_spec.rb'
+- './spec/lib/gitlab/auth/atlassian/auth_hash_spec.rb'
+- './spec/lib/gitlab/auth/atlassian/identity_linker_spec.rb'
+- './spec/lib/gitlab/auth/atlassian/user_spec.rb'
+- './spec/lib/gitlab/auth/auth_finders_spec.rb'
+- './spec/lib/gitlab/auth/blocked_user_tracker_spec.rb'
+- './spec/lib/gitlab/auth/crowd/authentication_spec.rb'
+- './spec/lib/gitlab/auth/current_user_mode_spec.rb'
+- './spec/lib/gitlab/auth/ip_rate_limiter_spec.rb'
+- './spec/lib/gitlab/auth/key_status_checker_spec.rb'
+- './spec/lib/gitlab/auth/ldap/access_spec.rb'
+- './spec/lib/gitlab/auth/ldap/adapter_spec.rb'
+- './spec/lib/gitlab/auth/ldap/authentication_spec.rb'
+- './spec/lib/gitlab/auth/ldap/auth_hash_spec.rb'
+- './spec/lib/gitlab/auth/ldap/config_spec.rb'
+- './spec/lib/gitlab/auth/ldap/dn_spec.rb'
+- './spec/lib/gitlab/auth/ldap/person_spec.rb'
+- './spec/lib/gitlab/auth/ldap/user_spec.rb'
+- './spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb'
+- './spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb'
+- './spec/lib/gitlab/auth/o_auth/provider_spec.rb'
+- './spec/lib/gitlab/auth/o_auth/user_spec.rb'
+- './spec/lib/gitlab/authorized_keys_spec.rb'
+- './spec/lib/gitlab/auth/otp/strategies/devise_spec.rb'
+- './spec/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp_spec.rb'
+- './spec/lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp_spec.rb'
+- './spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb'
+- './spec/lib/gitlab/auth/request_authenticator_spec.rb'
+- './spec/lib/gitlab/auth/result_spec.rb'
+- './spec/lib/gitlab/auth/saml/auth_hash_spec.rb'
+- './spec/lib/gitlab/auth/saml/config_spec.rb'
+- './spec/lib/gitlab/auth/saml/identity_linker_spec.rb'
+- './spec/lib/gitlab/auth/saml/origin_validator_spec.rb'
+- './spec/lib/gitlab/auth/saml/user_spec.rb'
+- './spec/lib/gitlab/auth_spec.rb'
+- './spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb'
+- './spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb'
+- './spec/lib/gitlab/auth/unique_ips_limiter_spec.rb'
+- './spec/lib/gitlab/auth/user_access_denied_reason_spec.rb'
+- './spec/lib/gitlab/avatar_cache_spec.rb'
+- './spec/lib/gitlab/background_migration/add_primary_email_to_emails_if_user_confirmed_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_group_features_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_issue_search_data_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_namespace_id_for_project_route_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_project_member_namespace_id_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_user_namespace_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb'
+- './spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb'
+- './spec/lib/gitlab/background_migration/base_job_spec.rb'
+- './spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/base_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb'
+- './spec/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex_spec.rb'
+- './spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/cleanup_orphaned_routes_spec.rb'
+- './spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
+- './spec/lib/gitlab/background_migration/delete_orphaned_deployments_spec.rb'
+- './spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb'
+- './spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb'
+- './spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/drop_invalid_security_findings_spec.rb'
+- './spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb'
+- './spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb'
+- './spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb'
+- './spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb'
+- './spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb'
+- './spec/lib/gitlab/background_migration/fix_duplicate_project_name_and_path_spec.rb'
+- './spec/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at_spec.rb'
+- './spec/lib/gitlab/background_migration/fix_merge_request_diff_commit_users_spec.rb'
+- './spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb'
+- './spec/lib/gitlab/background_migration/job_coordinator_spec.rb'
+- './spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
+- './spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
+- './spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb'
+- './spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb'
+- './spec/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users_spec.rb'
+- './spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb'
+- './spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb'
+- './spec/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category_spec.rb'
+- './spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb'
+- './spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb'
+- './spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb'
+- './spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb'
+- './spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb'
+- './spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb'
+- './spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb'
+- './spec/lib/gitlab/background_migration/populate_topics_total_projects_count_cache_spec.rb'
+- './spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb'
+- './spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb'
+- './spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb'
+- './spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb'
+- './spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb'
+- './spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
+- './spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb'
+- './spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb'
+- './spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb'
+- './spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb'
+- './spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb'
+- './spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
+- './spec/lib/gitlab/background_migration_spec.rb'
+- './spec/lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users_spec.rb'
+- './spec/lib/gitlab/background_migration/update_delayed_project_removal_to_null_for_user_namespaces_spec.rb'
+- './spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
+- './spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb'
+- './spec/lib/gitlab/background_migration/update_timelogs_project_id_spec.rb'
+- './spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb'
+- './spec/lib/gitlab/background_task_spec.rb'
+- './spec/lib/gitlab/backtrace_cleaner_spec.rb'
+- './spec/lib/gitlab/bare_repository_import/importer_spec.rb'
+- './spec/lib/gitlab/bare_repository_import/repository_spec.rb'
+- './spec/lib/gitlab/batch_pop_queueing_spec.rb'
+- './spec/lib/gitlab/batch_worker_context_spec.rb'
+- './spec/lib/gitlab/bitbucket_import/importer_spec.rb'
+- './spec/lib/gitlab/bitbucket_import/project_creator_spec.rb'
+- './spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb'
+- './spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
+- './spec/lib/gitlab/blame_spec.rb'
+- './spec/lib/gitlab/blob_helper_spec.rb'
+- './spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb'
+- './spec/lib/gitlab/buffered_io_spec.rb'
+- './spec/lib/gitlab/build_access_spec.rb'
+- './spec/lib/gitlab/bullet/exclusions_spec.rb'
+- './spec/lib/gitlab/bullet_spec.rb'
+- './spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb'
+- './spec/lib/gitlab/cache/helpers_spec.rb'
+- './spec/lib/gitlab/cache/import/caching_spec.rb'
+- './spec/lib/gitlab/cache/request_cache_spec.rb'
+- './spec/lib/gitlab/cache_spec.rb'
+- './spec/lib/gitlab/changelog/committer_spec.rb'
+- './spec/lib/gitlab/changelog/config_spec.rb'
+- './spec/lib/gitlab/changelog/generator_spec.rb'
+- './spec/lib/gitlab/changelog/release_spec.rb'
+- './spec/lib/gitlab/changes_list_spec.rb'
+- './spec/lib/gitlab/chat/command_spec.rb'
+- './spec/lib/gitlab/chat_name_token_spec.rb'
+- './spec/lib/gitlab/chat/output_spec.rb'
+- './spec/lib/gitlab/chat/responder/base_spec.rb'
+- './spec/lib/gitlab/chat/responder/mattermost_spec.rb'
+- './spec/lib/gitlab/chat/responder/slack_spec.rb'
+- './spec/lib/gitlab/chat/responder_spec.rb'
+- './spec/lib/gitlab/chat_spec.rb'
+- './spec/lib/gitlab/checks/branch_check_spec.rb'
+- './spec/lib/gitlab/checks/changes_access_spec.rb'
+- './spec/lib/gitlab/checks/container_moved_spec.rb'
+- './spec/lib/gitlab/checks/diff_check_spec.rb'
+- './spec/lib/gitlab/checks/force_push_spec.rb'
+- './spec/lib/gitlab/checks/lfs_check_spec.rb'
+- './spec/lib/gitlab/checks/lfs_integrity_spec.rb'
+- './spec/lib/gitlab/checks/matching_merge_request_spec.rb'
+- './spec/lib/gitlab/checks/project_created_spec.rb'
+- './spec/lib/gitlab/checks/push_check_spec.rb'
+- './spec/lib/gitlab/checks/push_file_count_check_spec.rb'
+- './spec/lib/gitlab/checks/single_change_access_spec.rb'
+- './spec/lib/gitlab/checks/snippet_check_spec.rb'
+- './spec/lib/gitlab/checks/tag_check_spec.rb'
+- './spec/lib/gitlab/checks/timed_logger_spec.rb'
+- './spec/lib/gitlab/ci_access_spec.rb'
+- './spec/lib/gitlab/ci/ansi2html_spec.rb'
+- './spec/lib/gitlab/ci/ansi2json/line_spec.rb'
+- './spec/lib/gitlab/ci/ansi2json/parser_spec.rb'
+- './spec/lib/gitlab/ci/ansi2json/result_spec.rb'
+- './spec/lib/gitlab/ci/ansi2json_spec.rb'
+- './spec/lib/gitlab/ci/ansi2json/style_spec.rb'
+- './spec/lib/gitlab/ci/artifact_file_reader_spec.rb'
+- './spec/lib/gitlab/ci/artifacts/logger_spec.rb'
+- './spec/lib/gitlab/ci/artifacts/metrics_spec.rb'
+- './spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb'
+- './spec/lib/gitlab/ci/badge/coverage/report_spec.rb'
+- './spec/lib/gitlab/ci/badge/coverage/template_spec.rb'
+- './spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb'
+- './spec/lib/gitlab/ci/badge/pipeline/status_spec.rb'
+- './spec/lib/gitlab/ci/badge/pipeline/template_spec.rb'
+- './spec/lib/gitlab/ci/badge/release/latest_release_spec.rb'
+- './spec/lib/gitlab/ci/badge/release/metadata_spec.rb'
+- './spec/lib/gitlab/ci/badge/release/template_spec.rb'
+- './spec/lib/gitlab/ci/build/artifacts/adapters/gzip_stream_spec.rb'
+- './spec/lib/gitlab/ci/build/artifacts/adapters/raw_stream_spec.rb'
+- './spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb'
+- './spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb'
+- './spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb'
+- './spec/lib/gitlab/ci/build/artifacts/path_spec.rb'
+- './spec/lib/gitlab/ci/build/auto_retry_spec.rb'
+- './spec/lib/gitlab/ci/build/cache_spec.rb'
+- './spec/lib/gitlab/ci/build/context/build_spec.rb'
+- './spec/lib/gitlab/ci/build/context/global_spec.rb'
+- './spec/lib/gitlab/ci/build/credentials/factory_spec.rb'
+- './spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb'
+- './spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb'
+- './spec/lib/gitlab/ci/build/duration_parser_spec.rb'
+- './spec/lib/gitlab/ci/build/image_spec.rb'
+- './spec/lib/gitlab/ci/build/policy/changes_spec.rb'
+- './spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb'
+- './spec/lib/gitlab/ci/build/policy/refs_spec.rb'
+- './spec/lib/gitlab/ci/build/policy_spec.rb'
+- './spec/lib/gitlab/ci/build/policy/variables_spec.rb'
+- './spec/lib/gitlab/ci/build/port_spec.rb'
+- './spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb'
+- './spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb'
+- './spec/lib/gitlab/ci/build/releaser_spec.rb'
+- './spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb'
+- './spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb'
+- './spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb'
+- './spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb'
+- './spec/lib/gitlab/ci/build/rules/rule_spec.rb'
+- './spec/lib/gitlab/ci/build/rules_spec.rb'
+- './spec/lib/gitlab/ci/build/status/reason_spec.rb'
+- './spec/lib/gitlab/ci/build/step_spec.rb'
+- './spec/lib/gitlab/ci/charts_spec.rb'
+- './spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/artifacts_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/bridge_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/cache_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/caches_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/commands_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/coverage_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/default_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/environment_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/files_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/hidden_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/imageable_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/image_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/include/rules_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/include_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/inherit/default_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/job_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/jobs_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/key_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/kubernetes_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/need_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/needs_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/paths_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/policy_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/port_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/ports_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/prefix_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/processable_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/product/variables_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/release/assets/link_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/release/assets/links_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/release/assets_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/release_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/reports_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/retry_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/root_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/rules_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/service_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/services_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/stage_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/stages_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/tags_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/trigger/forward_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/trigger_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/variables_spec.rb'
+- './spec/lib/gitlab/ci/config/entry/workflow_spec.rb'
+- './spec/lib/gitlab/ci/config/extendable/entry_spec.rb'
+- './spec/lib/gitlab/ci/config/extendable_spec.rb'
+- './spec/lib/gitlab/ci/config/external/context_spec.rb'
+- './spec/lib/gitlab/ci/config/external/file/artifact_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/file/project_spec.rb'
+- './spec/lib/gitlab/ci/config/external/file/remote_spec.rb'
+- './spec/lib/gitlab/ci/config/external/file/template_spec.rb'
+- './spec/lib/gitlab/ci/config/external/mapper_spec.rb'
+- './spec/lib/gitlab/ci/config/external/processor_spec.rb'
+- './spec/lib/gitlab/ci/config/external/rules_spec.rb'
+- './spec/lib/gitlab/ci/config/normalizer/factory_spec.rb'
+- './spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb'
+- './spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb'
+- './spec/lib/gitlab/ci/config/normalizer_spec.rb'
+- './spec/lib/gitlab/ci/config_spec.rb'
+- './spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb'
+- './spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb'
+- './spec/lib/gitlab/ci/cron_parser_spec.rb'
+- './spec/lib/gitlab/ci/jwt_spec.rb'
+- './spec/lib/gitlab/ci/jwt_v2_spec.rb'
+- './spec/lib/gitlab/ci/lint_spec.rb'
+- './spec/lib/gitlab/ci/mask_secret_spec.rb'
+- './spec/lib/gitlab/ci/matching/build_matcher_spec.rb'
+- './spec/lib/gitlab/ci/matching/runner_matcher_spec.rb'
+- './spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
+- './spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
+- './spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb'
+- './spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb'
+- './spec/lib/gitlab/ci/parsers/instrumentation_spec.rb'
+- './spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb'
+- './spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb'
+- './spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb'
+- './spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb'
+- './spec/lib/gitlab/ci/parsers/security/common_spec.rb'
+- './spec/lib/gitlab/ci/parsers/security/sast_spec.rb'
+- './spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb'
+- './spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
+- './spec/lib/gitlab/ci/parsers_spec.rb'
+- './spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb'
+- './spec/lib/gitlab/ci/parsers/test/junit_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/build_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/command_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/create_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/limit/rate_limit_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/pipeline/process_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/duration_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/expression/token_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/logger_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/metrics_spec.rb'
+- './spec/lib/gitlab/ci/pipeline_object_hierarchy_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/preloader_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/build_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb'
+- './spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb'
+- './spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb'
+- './spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
+- './spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb'
+- './spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb'
+- './spec/lib/gitlab/ci/reports/codequality_reports_spec.rb'
+- './spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb'
+- './spec/lib/gitlab/ci/reports/coverage_report_spec.rb'
+- './spec/lib/gitlab/ci/reports/reports_comparer_spec.rb'
+- './spec/lib/gitlab/ci/reports/sbom/component_spec.rb'
+- './spec/lib/gitlab/ci/reports/sbom/report_spec.rb'
+- './spec/lib/gitlab/ci/reports/sbom/reports_spec.rb'
+- './spec/lib/gitlab/ci/reports/sbom/source_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/finding_key_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/flag_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/identifier_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/link_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/report_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/reports_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/scanner_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/scan_spec.rb'
+- './spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb'
+- './spec/lib/gitlab/ci/reports/terraform_reports_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_case_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_failure_history_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_report_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_report_summary_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_suite_spec.rb'
+- './spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb'
+- './spec/lib/gitlab/ci/runner/backoff_spec.rb'
+- './spec/lib/gitlab/ci/runner_instructions_spec.rb'
+- './spec/lib/gitlab/ci/runner/metrics_spec.rb'
+- './spec/lib/gitlab/ci/runner_releases_spec.rb'
+- './spec/lib/gitlab/ci/runner_upgrade_check_spec.rb'
+- './spec/lib/gitlab/ci/status/bridge/common_spec.rb'
+- './spec/lib/gitlab/ci/status/bridge/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb'
+- './spec/lib/gitlab/ci/status/build/action_spec.rb'
+- './spec/lib/gitlab/ci/status/build/cancelable_spec.rb'
+- './spec/lib/gitlab/ci/status/build/canceled_spec.rb'
+- './spec/lib/gitlab/ci/status/build/common_spec.rb'
+- './spec/lib/gitlab/ci/status/build/created_spec.rb'
+- './spec/lib/gitlab/ci/status/build/erased_spec.rb'
+- './spec/lib/gitlab/ci/status/build/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb'
+- './spec/lib/gitlab/ci/status/build/failed_spec.rb'
+- './spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb'
+- './spec/lib/gitlab/ci/status/build/manual_spec.rb'
+- './spec/lib/gitlab/ci/status/build/pending_spec.rb'
+- './spec/lib/gitlab/ci/status/build/play_spec.rb'
+- './spec/lib/gitlab/ci/status/build/preparing_spec.rb'
+- './spec/lib/gitlab/ci/status/build/retried_spec.rb'
+- './spec/lib/gitlab/ci/status/build/retryable_spec.rb'
+- './spec/lib/gitlab/ci/status/build/scheduled_spec.rb'
+- './spec/lib/gitlab/ci/status/build/skipped_spec.rb'
+- './spec/lib/gitlab/ci/status/build/stop_spec.rb'
+- './spec/lib/gitlab/ci/status/build/unschedule_spec.rb'
+- './spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb'
+- './spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb'
+- './spec/lib/gitlab/ci/status/canceled_spec.rb'
+- './spec/lib/gitlab/ci/status/composite_spec.rb'
+- './spec/lib/gitlab/ci/status/core_spec.rb'
+- './spec/lib/gitlab/ci/status/created_spec.rb'
+- './spec/lib/gitlab/ci/status/extended_spec.rb'
+- './spec/lib/gitlab/ci/status/external/common_spec.rb'
+- './spec/lib/gitlab/ci/status/external/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/failed_spec.rb'
+- './spec/lib/gitlab/ci/status/group/common_spec.rb'
+- './spec/lib/gitlab/ci/status/group/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/manual_spec.rb'
+- './spec/lib/gitlab/ci/status/pending_spec.rb'
+- './spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb'
+- './spec/lib/gitlab/ci/status/pipeline/common_spec.rb'
+- './spec/lib/gitlab/ci/status/pipeline/delayed_spec.rb'
+- './spec/lib/gitlab/ci/status/pipeline/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/preparing_spec.rb'
+- './spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb'
+- './spec/lib/gitlab/ci/status/running_spec.rb'
+- './spec/lib/gitlab/ci/status/scheduled_spec.rb'
+- './spec/lib/gitlab/ci/status/skipped_spec.rb'
+- './spec/lib/gitlab/ci/status/stage/common_spec.rb'
+- './spec/lib/gitlab/ci/status/stage/factory_spec.rb'
+- './spec/lib/gitlab/ci/status/stage/play_manual_spec.rb'
+- './spec/lib/gitlab/ci/status/success_spec.rb'
+- './spec/lib/gitlab/ci/status/success_warning_spec.rb'
+- './spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb'
+- './spec/lib/gitlab/ci/tags/bulk_insert_spec.rb'
+- './spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/flutter_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/sast_iac_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/MATLAB_spec.rb'
+- './spec/lib/gitlab/ci/templates/npm_spec.rb'
+- './spec/lib/gitlab/ci/templates/templates_spec.rb'
+- './spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb'
+- './spec/lib/gitlab/ci/trace/archive_spec.rb'
+- './spec/lib/gitlab/ci/trace/backoff_spec.rb'
+- './spec/lib/gitlab/ci/trace/checksum_spec.rb'
+- './spec/lib/gitlab/ci/trace/chunked_io_spec.rb'
+- './spec/lib/gitlab/ci/trace/metrics_spec.rb'
+- './spec/lib/gitlab/ci/trace/remote_checksum_spec.rb'
+- './spec/lib/gitlab/ci/trace/section_parser_spec.rb'
+- './spec/lib/gitlab/ci/trace_spec.rb'
+- './spec/lib/gitlab/ci/trace/stream_spec.rb'
+- './spec/lib/gitlab/ci/variables/builder/group_spec.rb'
+- './spec/lib/gitlab/ci/variables/builder/instance_spec.rb'
+- './spec/lib/gitlab/ci/variables/builder/project_spec.rb'
+- './spec/lib/gitlab/ci/variables/builder_spec.rb'
+- './spec/lib/gitlab/ci/variables/collection/item_spec.rb'
+- './spec/lib/gitlab/ci/variables/collection/sort_spec.rb'
+- './spec/lib/gitlab/ci/variables/collection_spec.rb'
+- './spec/lib/gitlab/ci/variables/helpers_spec.rb'
+- './spec/lib/gitlab/ci/yaml_processor/dag_spec.rb'
+- './spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb'
+- './spec/lib/gitlab/ci/yaml_processor/result_spec.rb'
+- './spec/lib/gitlab/ci/yaml_processor_spec.rb'
+- './spec/lib/gitlab/class_attributes_spec.rb'
+- './spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb'
+- './spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb'
+- './spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb'
+- './spec/lib/gitlab/cleanup/project_uploads_spec.rb'
+- './spec/lib/gitlab/cleanup/remote_uploads_spec.rb'
+- './spec/lib/gitlab/closing_issue_extractor_spec.rb'
+- './spec/lib/gitlab/cluster/lifecycle_events_spec.rb'
+- './spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb'
+- './spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb'
+- './spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb'
+- './spec/lib/gitlab/code_navigation_path_spec.rb'
+- './spec/lib/gitlab/color_schemes_spec.rb'
+- './spec/lib/gitlab/color_spec.rb'
+- './spec/lib/gitlab/composer/cache_spec.rb'
+- './spec/lib/gitlab/composer/version_index_spec.rb'
+- './spec/lib/gitlab/conan_token_spec.rb'
+- './spec/lib/gitlab/config_checker/external_database_checker_spec.rb'
+- './spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
+- './spec/lib/gitlab/config/entry/attributable_spec.rb'
+- './spec/lib/gitlab/config/entry/boolean_spec.rb'
+- './spec/lib/gitlab/config/entry/composable_array_spec.rb'
+- './spec/lib/gitlab/config/entry/composable_hash_spec.rb'
+- './spec/lib/gitlab/config/entry/configurable_spec.rb'
+- './spec/lib/gitlab/config/entry/factory_spec.rb'
+- './spec/lib/gitlab/config/entry/simplifiable_spec.rb'
+- './spec/lib/gitlab/config/entry/undefined_spec.rb'
+- './spec/lib/gitlab/config/entry/unspecified_spec.rb'
+- './spec/lib/gitlab/config/entry/validatable_spec.rb'
+- './spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb'
+- './spec/lib/gitlab/config/entry/validator_spec.rb'
+- './spec/lib/gitlab/config/entry/validators_spec.rb'
+- './spec/lib/gitlab/config/loader/yaml_spec.rb'
+- './spec/lib/gitlab/conflict/file_collection_spec.rb'
+- './spec/lib/gitlab/conflict/file_spec.rb'
+- './spec/lib/gitlab/console_spec.rb'
+- './spec/lib/gitlab/consul/internal_spec.rb'
+- './spec/lib/gitlab/container_repository/tags/cache_spec.rb'
+- './spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
+- './spec/lib/gitlab/contributions_calendar_spec.rb'
+- './spec/lib/gitlab/cross_project_access/check_collection_spec.rb'
+- './spec/lib/gitlab/cross_project_access/check_info_spec.rb'
+- './spec/lib/gitlab/cross_project_access/class_methods_spec.rb'
+- './spec/lib/gitlab/cross_project_access_spec.rb'
+- './spec/lib/gitlab/crypto_helper_spec.rb'
+- './spec/lib/gitlab/current_settings_spec.rb'
+- './spec/lib/gitlab/cycle_analytics/permissions_spec.rb'
+- './spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
+- './spec/lib/gitlab/cycle_analytics/summary/value_spec.rb'
+- './spec/lib/gitlab/cycle_analytics/updater_spec.rb'
+- './spec/lib/gitlab/daemon_spec.rb'
+- './spec/lib/gitlab/database/async_indexes/index_creator_spec.rb'
+- './spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb'
+- './spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb'
+- './spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb'
+- './spec/lib/gitlab/database/async_indexes_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batched_job_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batched_migration_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb'
+- './spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb'
+- './spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb'
+- './spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb'
+- './spec/lib/gitlab/database/background_migration/health_status_spec.rb'
+- './spec/lib/gitlab/database/background_migration_job_spec.rb'
+- './spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb'
+- './spec/lib/gitlab/database/batch_count_spec.rb'
+- './spec/lib/gitlab/database/bulk_update_spec.rb'
+- './spec/lib/gitlab/database/connection_timer_spec.rb'
+- './spec/lib/gitlab/database/consistency_checker_spec.rb'
+- './spec/lib/gitlab/database/consistency_spec.rb'
+- './spec/lib/gitlab/database/count/exact_count_strategy_spec.rb'
+- './spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb'
+- './spec/lib/gitlab/database/count_spec.rb'
+- './spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb'
+- './spec/lib/gitlab/database/dynamic_model_helpers_spec.rb'
+- './spec/lib/gitlab/database/each_database_spec.rb'
+- './spec/lib/gitlab/database/gitlab_schema_spec.rb'
+- './spec/lib/gitlab/database/grant_spec.rb'
+- './spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
+- './spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb'
+- './spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb'
+- './spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb'
+- './spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb'
+- './spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/configuration_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/host_list_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/host_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/primary_host_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/resolver_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/session_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/setup_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb'
+- './spec/lib/gitlab/database/load_balancing_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/srv_resolver_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/sticking_spec.rb'
+- './spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb'
+- './spec/lib/gitlab/database/lock_writes_manager_spec.rb'
+- './spec/lib/gitlab/database/loose_foreign_keys_spec.rb'
+- './spec/lib/gitlab/database/migration_helpers/announce_database_spec.rb'
+- './spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb'
+- './spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb'
+- './spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb'
+- './spec/lib/gitlab/database/migration_helpers_spec.rb'
+- './spec/lib/gitlab/database/migration_helpers/v2_spec.rb'
+- './spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb'
+- './spec/lib/gitlab/database/migrations/base_background_runner_spec.rb'
+- './spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb'
+- './spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
+- './spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb'
+- './spec/lib/gitlab/database/migrations/observers/query_details_spec.rb'
+- './spec/lib/gitlab/database/migrations/observers/query_log_spec.rb'
+- './spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb'
+- './spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb'
+- './spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb'
+- './spec/lib/gitlab/database/migration_spec.rb'
+- './spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb'
+- './spec/lib/gitlab/database/migrations/runner_spec.rb'
+- './spec/lib/gitlab/database/migrations/test_background_runner_spec.rb'
+- './spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb'
+- './spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb'
+- './spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb'
+- './spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb'
+- './spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb'
+- './spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb'
+- './spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb'
+- './spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
+- './spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb'
+- './spec/lib/gitlab/database/partitioning/partition_manager_spec.rb'
+- './spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb'
+- './spec/lib/gitlab/database/partitioning/replace_table_spec.rb'
+- './spec/lib/gitlab/database/partitioning/single_numeric_list_partition_spec.rb'
+- './spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb'
+- './spec/lib/gitlab/database/partitioning_spec.rb'
+- './spec/lib/gitlab/database/partitioning/time_partition_spec.rb'
+- './spec/lib/gitlab/database/pg_class_spec.rb'
+- './spec/lib/gitlab/database/postgres_autovacuum_activity_spec.rb'
+- './spec/lib/gitlab/database/postgres_foreign_key_spec.rb'
+- './spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb'
+- './spec/lib/gitlab/database/postgres_hll/buckets_spec.rb'
+- './spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb'
+- './spec/lib/gitlab/database/postgres_index_spec.rb'
+- './spec/lib/gitlab/database/postgres_partitioned_table_spec.rb'
+- './spec/lib/gitlab/database/postgres_partition_spec.rb'
+- './spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb'
+- './spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
+- './spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb'
+- './spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb'
+- './spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb'
+- './spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb'
+- './spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb'
+- './spec/lib/gitlab/database/query_analyzer_spec.rb'
+- './spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb'
+- './spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb'
+- './spec/lib/gitlab/database/reflection_spec.rb'
+- './spec/lib/gitlab/database/reindexing/coordinator_spec.rb'
+- './spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
+- './spec/lib/gitlab/database/reindexing/index_selection_spec.rb'
+- './spec/lib/gitlab/database/reindexing/reindex_action_spec.rb'
+- './spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
+- './spec/lib/gitlab/database/reindexing_spec.rb'
+- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
+- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb'
+- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb'
+- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb'
+- './spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb'
+- './spec/lib/gitlab/database/schema_cleaner_spec.rb'
+- './spec/lib/gitlab/database/schema_migrations/context_spec.rb'
+- './spec/lib/gitlab/database/schema_migrations/migrations_spec.rb'
+- './spec/lib/gitlab/database/sha_attribute_spec.rb'
+- './spec/lib/gitlab/database/shared_model_spec.rb'
+- './spec/lib/gitlab/database/similarity_score_spec.rb'
+- './spec/lib/gitlab/database_spec.rb'
+- './spec/lib/gitlab/database/transaction/context_spec.rb'
+- './spec/lib/gitlab/database/transaction/observer_spec.rb'
+- './spec/lib/gitlab/database/type/color_spec.rb'
+- './spec/lib/gitlab/database/type/json_pg_safe_spec.rb'
+- './spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb'
+- './spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
+- './spec/lib/gitlab/database/with_lock_retries_spec.rb'
+- './spec/lib/gitlab/data_builder/alert_spec.rb'
+- './spec/lib/gitlab/data_builder/archive_trace_spec.rb'
+- './spec/lib/gitlab/data_builder/build_spec.rb'
+- './spec/lib/gitlab/data_builder/deployment_spec.rb'
+- './spec/lib/gitlab/data_builder/feature_flag_spec.rb'
+- './spec/lib/gitlab/data_builder/issuable_spec.rb'
+- './spec/lib/gitlab/data_builder/note_spec.rb'
+- './spec/lib/gitlab/data_builder/pipeline_spec.rb'
+- './spec/lib/gitlab/data_builder/push_spec.rb'
+- './spec/lib/gitlab/data_builder/wiki_page_spec.rb'
+- './spec/lib/gitlab/default_branch_spec.rb'
+- './spec/lib/gitlab/dependency_linker/base_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb'
+- './spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb'
+- './spec/lib/gitlab/dependency_linker_spec.rb'
+- './spec/lib/gitlab/deploy_key_access_spec.rb'
+- './spec/lib/gitlab/diff/char_diff_spec.rb'
+- './spec/lib/gitlab/diff/diff_refs_spec.rb'
+- './spec/lib/gitlab/diff/file_collection/base_spec.rb'
+- './spec/lib/gitlab/diff/file_collection/commit_spec.rb'
+- './spec/lib/gitlab/diff/file_collection/compare_spec.rb'
+- './spec/lib/gitlab/diff/file_collection/merge_request_diff_base_spec.rb'
+- './spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb'
+- './spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb'
+- './spec/lib/gitlab/diff/file_collection_sorter_spec.rb'
+- './spec/lib/gitlab/diff/file_spec.rb'
+- './spec/lib/gitlab/diff/formatters/image_formatter_spec.rb'
+- './spec/lib/gitlab/diff/formatters/text_formatter_spec.rb'
+- './spec/lib/gitlab/diff/highlight_cache_spec.rb'
+- './spec/lib/gitlab/diff/highlight_spec.rb'
+- './spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb'
+- './spec/lib/gitlab/diff/inline_diff_marker_spec.rb'
+- './spec/lib/gitlab/diff/inline_diff_spec.rb'
+- './spec/lib/gitlab/diff/line_mapper_spec.rb'
+- './spec/lib/gitlab/diff/line_spec.rb'
+- './spec/lib/gitlab/diff/lines_unfolder_spec.rb'
+- './spec/lib/gitlab/diff/pair_selector_spec.rb'
+- './spec/lib/gitlab/diff/parallel_diff_spec.rb'
+- './spec/lib/gitlab/diff/parser_spec.rb'
+- './spec/lib/gitlab/diff/position_collection_spec.rb'
+- './spec/lib/gitlab/diff/position_spec.rb'
+- './spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb'
+- './spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb'
+- './spec/lib/gitlab/diff/position_tracer_spec.rb'
+- './spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb'
+- './spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb'
+- './spec/lib/gitlab/diff/stats_cache_spec.rb'
+- './spec/lib/gitlab/diff/suggestion_diff_spec.rb'
+- './spec/lib/gitlab/diff/suggestions_parser_spec.rb'
+- './spec/lib/gitlab/diff/suggestion_spec.rb'
+- './spec/lib/gitlab/discussions_diff/file_collection_spec.rb'
+- './spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb'
+- './spec/lib/gitlab/doctor/secrets_spec.rb'
+- './spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb'
+- './spec/lib/gitlab_edition_spec.rb'
+- './spec/lib/gitlab/email/attachment_uploader_spec.rb'
+- './spec/lib/gitlab/email/failure_handler_spec.rb'
+- './spec/lib/gitlab/email/handler/create_issue_handler_spec.rb'
+- './spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb'
+- './spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
+- './spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb'
+- './spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
+- './spec/lib/gitlab/email/handler_spec.rb'
+- './spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb'
+- './spec/lib/gitlab/email/hook/additional_headers_interceptor_spec.rb'
+- './spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb'
+- './spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb'
+- './spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb'
+- './spec/lib/gitlab/email/hook/validate_addresses_interceptor_spec.rb'
+- './spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb'
+- './spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb'
+- './spec/lib/gitlab/email/message/repository_push_spec.rb'
+- './spec/lib/gitlab/email/receiver_spec.rb'
+- './spec/lib/gitlab/email/reply_parser_spec.rb'
+- './spec/lib/gitlab/email/service_desk_receiver_spec.rb'
+- './spec/lib/gitlab/email/smime/signer_spec.rb'
+- './spec/lib/gitlab/emoji_spec.rb'
+- './spec/lib/gitlab/empty_search_results_spec.rb'
+- './spec/lib/gitlab/encoding_helper_spec.rb'
+- './spec/lib/gitlab/encrypted_configuration_spec.rb'
+- './spec/lib/gitlab/endpoint_attributes_spec.rb'
+- './spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb'
+- './spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb'
+- './spec/lib/gitlab/error_tracking/log_formatter_spec.rb'
+- './spec/lib/gitlab/error_tracking/logger_spec.rb'
+- './spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb'
+- './spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb'
+- './spec/lib/gitlab/error_tracking/processor/sanitize_error_message_processor_spec.rb'
+- './spec/lib/gitlab/error_tracking/processor/sanitizer_processor_spec.rb'
+- './spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb'
+- './spec/lib/gitlab/error_tracking_spec.rb'
+- './spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb'
+- './spec/lib/gitlab/etag_caching/middleware_spec.rb'
+- './spec/lib/gitlab/etag_caching/router/graphql_spec.rb'
+- './spec/lib/gitlab/etag_caching/router/rails_spec.rb'
+- './spec/lib/gitlab/etag_caching/router_spec.rb'
+- './spec/lib/gitlab/etag_caching/store_spec.rb'
+- './spec/lib/gitlab/event_store/event_spec.rb'
+- './spec/lib/gitlab/event_store/store_spec.rb'
+- './spec/lib/gitlab/exception_log_formatter_spec.rb'
+- './spec/lib/gitlab/exceptions_app_spec.rb'
+- './spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb'
+- './spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
+- './spec/lib/gitlab/exclusive_lease_spec.rb'
+- './spec/lib/gitlab/experimentation/controller_concern_spec.rb'
+- './spec/lib/gitlab/experimentation/experiment_spec.rb'
+- './spec/lib/gitlab/experimentation/group_types_spec.rb'
+- './spec/lib/gitlab/experimentation_spec.rb'
+- './spec/lib/gitlab/experiment/rollout/feature_spec.rb'
+- './spec/lib/gitlab/external_authorization/access_spec.rb'
+- './spec/lib/gitlab/external_authorization/cache_spec.rb'
+- './spec/lib/gitlab/external_authorization/client_spec.rb'
+- './spec/lib/gitlab/external_authorization/logger_spec.rb'
+- './spec/lib/gitlab/external_authorization/response_spec.rb'
+- './spec/lib/gitlab/external_authorization_spec.rb'
+- './spec/lib/gitlab/fake_application_settings_spec.rb'
+- './spec/lib/gitlab/faraday/error_callback_spec.rb'
+- './spec/lib/gitlab/favicon_spec.rb'
+- './spec/lib/gitlab/feature_categories_spec.rb'
+- './spec/lib/gitlab/file_detector_spec.rb'
+- './spec/lib/gitlab/file_finder_spec.rb'
+- './spec/lib/gitlab/file_hook_spec.rb'
+- './spec/lib/gitlab/file_markdown_link_builder_spec.rb'
+- './spec/lib/gitlab/file_type_detection_spec.rb'
+- './spec/lib/gitlab/fips_spec.rb'
+- './spec/lib/gitlab/fogbugz_import/client_spec.rb'
+- './spec/lib/gitlab/fogbugz_import/importer_spec.rb'
+- './spec/lib/gitlab/fogbugz_import/project_creator_spec.rb'
+- './spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb'
+- './spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
+- './spec/lib/gitlab/gfm/uploads_rewriter_spec.rb'
+- './spec/lib/gitlab/git_access_design_spec.rb'
+- './spec/lib/gitlab/git_access_project_spec.rb'
+- './spec/lib/gitlab/git_access_snippet_spec.rb'
+- './spec/lib/gitlab/git_access_spec.rb'
+- './spec/lib/gitlab/git_access_wiki_spec.rb'
+- './spec/lib/gitlab/gitaly_client/blob_service_spec.rb'
+- './spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb'
+- './spec/lib/gitlab/gitaly_client/call_spec.rb'
+- './spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb'
+- './spec/lib/gitlab/gitaly_client/commit_service_spec.rb'
+- './spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb'
+- './spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb'
+- './spec/lib/gitlab/gitaly_client/diff_spec.rb'
+- './spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb'
+- './spec/lib/gitlab/gitaly_client/health_check_service_spec.rb'
+- './spec/lib/gitlab/gitaly_client/object_pool_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/server_service_spec.rb'
+- './spec/lib/gitlab/gitaly_client_spec.rb'
+- './spec/lib/gitlab/gitaly_client/storage_settings_spec.rb'
+- './spec/lib/gitlab/gitaly_client/util_spec.rb'
+- './spec/lib/gitlab/gitaly_client/wiki_service_spec.rb'
+- './spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb'
+- './spec/lib/gitlab/git/attributes_parser_spec.rb'
+- './spec/lib/gitlab/git/base_error_spec.rb'
+- './spec/lib/gitlab/git/blame_spec.rb'
+- './spec/lib/gitlab/git/blob_spec.rb'
+- './spec/lib/gitlab/git/branch_spec.rb'
+- './spec/lib/gitlab/git/bundle_file_spec.rb'
+- './spec/lib/gitlab/git/changed_path_spec.rb'
+- './spec/lib/gitlab/git/changes_spec.rb'
+- './spec/lib/gitlab/git/commit_spec.rb'
+- './spec/lib/gitlab/git/commit_stats_spec.rb'
+- './spec/lib/gitlab/git/compare_spec.rb'
+- './spec/lib/gitlab/git/conflict/file_spec.rb'
+- './spec/lib/gitlab/git/conflict/parser_spec.rb'
+- './spec/lib/gitlab/git/conflict/resolver_spec.rb'
+- './spec/lib/gitlab/git/cross_repo_comparer_spec.rb'
+- './spec/lib/gitlab/git/diff_collection_spec.rb'
+- './spec/lib/gitlab/git/diff_spec.rb'
+- './spec/lib/gitlab/git/diff_stats_collection_spec.rb'
+- './spec/lib/gitlab/git/gitmodules_parser_spec.rb'
+- './spec/lib/gitlab/git/hook_env_spec.rb'
+- './spec/lib/gitlab/github_import/bulk_importing_spec.rb'
+- './spec/lib/gitlab/github_import/client_spec.rb'
+- './spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/closed_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/renamed_spec.rb'
+- './spec/lib/gitlab/github_import/importer/events/reopened_spec.rb'
+- './spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/issue_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/issues_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/labels_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/notes_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/releases_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/repository_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb'
+- './spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb'
+- './spec/lib/gitlab/github_import/issuable_finder_spec.rb'
+- './spec/lib/gitlab/github_import/label_finder_spec.rb'
+- './spec/lib/gitlab/github_import/logger_spec.rb'
+- './spec/lib/gitlab/github_import/markdown_text_spec.rb'
+- './spec/lib/gitlab/github_import/milestone_finder_spec.rb'
+- './spec/lib/gitlab/github_import/object_counter_spec.rb'
+- './spec/lib/gitlab/github_import/page_counter_spec.rb'
+- './spec/lib/gitlab/github_import/parallel_importer_spec.rb'
+- './spec/lib/gitlab/github_import/parallel_scheduling_spec.rb'
+- './spec/lib/gitlab/github_import/representation/diff_note_spec.rb'
+- './spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb'
+- './spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb'
+- './spec/lib/gitlab/github_import/representation/issue_event_spec.rb'
+- './spec/lib/gitlab/github_import/representation/issue_spec.rb'
+- './spec/lib/gitlab/github_import/representation/lfs_object_spec.rb'
+- './spec/lib/gitlab/github_import/representation/note_spec.rb'
+- './spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb'
+- './spec/lib/gitlab/github_import/representation/pull_request_spec.rb'
+- './spec/lib/gitlab/github_import/representation_spec.rb'
+- './spec/lib/gitlab/github_import/representation/to_hash_spec.rb'
+- './spec/lib/gitlab/github_import/representation/user_spec.rb'
+- './spec/lib/gitlab/github_import/sequential_importer_spec.rb'
+- './spec/lib/gitlab/github_import/single_endpoint_notes_importing_spec.rb'
+- './spec/lib/gitlab/github_import_spec.rb'
+- './spec/lib/gitlab/github_import/user_finder_spec.rb'
+- './spec/lib/gitlab/git/keep_around_spec.rb'
+- './spec/lib/gitlab/gitlab_import/client_spec.rb'
+- './spec/lib/gitlab/gitlab_import/importer_spec.rb'
+- './spec/lib/gitlab/gitlab_import/project_creator_spec.rb'
+- './spec/lib/gitlab/git/lfs_changes_spec.rb'
+- './spec/lib/gitlab/git/lfs_pointer_file_spec.rb'
+- './spec/lib/gitlab/git/merge_base_spec.rb'
+- './spec/lib/gitlab/git/object_pool_spec.rb'
+- './spec/lib/gitlab/git/patches/collection_spec.rb'
+- './spec/lib/gitlab/git/patches/commit_patches_spec.rb'
+- './spec/lib/gitlab/git/patches/patch_spec.rb'
+- './spec/lib/gitlab/git_post_receive_spec.rb'
+- './spec/lib/gitlab/git/pre_receive_error_spec.rb'
+- './spec/lib/gitlab/git/push_spec.rb'
+- './spec/lib/gitlab/git/raw_diff_change_spec.rb'
+- './spec/lib/gitlab/git_ref_validator_spec.rb'
+- './spec/lib/gitlab/git/remote_mirror_spec.rb'
+- './spec/lib/gitlab/git/repository_cleaner_spec.rb'
+- './spec/lib/gitlab/git/repository_spec.rb'
+- './spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
+- './spec/lib/gitlab/git_spec.rb'
+- './spec/lib/gitlab/git/tag_spec.rb'
+- './spec/lib/gitlab/git/tree_spec.rb'
+- './spec/lib/gitlab/git/user_spec.rb'
+- './spec/lib/gitlab/git/util_spec.rb'
+- './spec/lib/gitlab/git/wiki_page_version_spec.rb'
+- './spec/lib/gitlab/git/wiki_spec.rb'
+- './spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb'
+- './spec/lib/gitlab/global_id/deprecations_spec.rb'
+- './spec/lib/gitlab/global_id_spec.rb'
+- './spec/lib/gitlab/gl_repository/identifier_spec.rb'
+- './spec/lib/gitlab/gl_repository/repo_type_spec.rb'
+- './spec/lib/gitlab/gl_repository_spec.rb'
+- './spec/lib/gitlab/gon_helper_spec.rb'
+- './spec/lib/gitlab/gpg/commit_spec.rb'
+- './spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb'
+- './spec/lib/gitlab/gpg_spec.rb'
+- './spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb'
+- './spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb'
+- './spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb'
+- './spec/lib/gitlab/graphql/authorize/object_authorization_spec.rb'
+- './spec/lib/gitlab/graphql/batch_key_spec.rb'
+- './spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb'
+- './spec/lib/gitlab/graphql/copy_field_description_spec.rb'
+- './spec/lib/gitlab/graphql/deprecation_spec.rb'
+- './spec/lib/gitlab/graphql/generic_tracing_spec.rb'
+- './spec/lib/gitlab/graphql/known_operations_spec.rb'
+- './spec/lib/gitlab/graphql/lazy_spec.rb'
+- './spec/lib/gitlab/graphql/loaders/batch_commit_loader_spec.rb'
+- './spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb'
+- './spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb'
+- './spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb'
+- './spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb'
+- './spec/lib/gitlab/graphql/loaders/issuable_loader_spec.rb'
+- './spec/lib/gitlab/graphql_logger_spec.rb'
+- './spec/lib/gitlab/graphql/markdown_field_spec.rb'
+- './spec/lib/gitlab/graphql/mount_mutation_spec.rb'
+- './spec/lib/gitlab/graphql/negatable_arguments_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/active_record_array_connection_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/array_connection_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/connections_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb'
+- './spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb'
+- './spec/lib/gitlab/graphql/present/field_extension_spec.rb'
+- './spec/lib/gitlab/graphql/queries_spec.rb'
+- './spec/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer_spec.rb'
+- './spec/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer_spec.rb'
+- './spec/lib/gitlab/graphql/representation/submodule_tree_entry_spec.rb'
+- './spec/lib/gitlab/graphql/representation/tree_entry_spec.rb'
+- './spec/lib/gitlab/graphql/timeout_spec.rb'
+- './spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb'
+- './spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb'
+- './spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb'
+- './spec/lib/gitlab/graphql/tracers/timer_tracer_spec.rb'
+- './spec/lib/gitlab/graphql/type_name_deprecations_spec.rb'
+- './spec/lib/gitlab/graphs/commits_spec.rb'
+- './spec/lib/gitlab/group_search_results_spec.rb'
+- './spec/lib/gitlab/harbor/client_spec.rb'
+- './spec/lib/gitlab/harbor/query_spec.rb'
+- './spec/lib/gitlab/hashed_path_spec.rb'
+- './spec/lib/gitlab/hashed_storage/migrator_spec.rb'
+- './spec/lib/gitlab/health_checks/db_check_spec.rb'
+- './spec/lib/gitlab/health_checks/gitaly_check_spec.rb'
+- './spec/lib/gitlab/health_checks/master_check_spec.rb'
+- './spec/lib/gitlab/health_checks/middleware_spec.rb'
+- './spec/lib/gitlab/health_checks/probes/collection_spec.rb'
+- './spec/lib/gitlab/health_checks/puma_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/cache_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/queues_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/redis_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/sessions_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb'
+- './spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb'
+- './spec/lib/gitlab/health_checks/server_spec.rb'
+- './spec/lib/gitlab/highlight_spec.rb'
+- './spec/lib/gitlab/hook_data/base_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/group_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/group_member_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/issue_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/key_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/merge_request_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/project_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/project_member_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/release_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/subgroup_builder_spec.rb'
+- './spec/lib/gitlab/hook_data/user_builder_spec.rb'
+- './spec/lib/gitlab/hotlinking_detector_spec.rb'
+- './spec/lib/gitlab/http_connection_adapter_spec.rb'
+- './spec/lib/gitlab/http_io_spec.rb'
+- './spec/lib/gitlab/http_spec.rb'
+- './spec/lib/gitlab/i18n/metadata_entry_spec.rb'
+- './spec/lib/gitlab/i18n/po_linter_spec.rb'
+- './spec/lib/gitlab/i18n_spec.rb'
+- './spec/lib/gitlab/i18n/translation_entry_spec.rb'
+- './spec/lib/gitlab/identifier_spec.rb'
+- './spec/lib/gitlab/import/database_helpers_spec.rb'
+- './spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb'
+- './spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb'
+- './spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb'
+- './spec/lib/gitlab/import_export/attribute_cleaner_spec.rb'
+- './spec/lib/gitlab/import_export/attribute_configuration_spec.rb'
+- './spec/lib/gitlab/import_export/attributes_finder_spec.rb'
+- './spec/lib/gitlab/import_export/attributes_permitter_spec.rb'
+- './spec/lib/gitlab/import_export/avatar_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/avatar_saver_spec.rb'
+- './spec/lib/gitlab/import_export/base/object_builder_spec.rb'
+- './spec/lib/gitlab/import_export/base/relation_factory_spec.rb'
+- './spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb'
+- './spec/lib/gitlab/import_export/command_line_util_spec.rb'
+- './spec/lib/gitlab/import_export/config_spec.rb'
+- './spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb'
+- './spec/lib/gitlab/import_export/design_repo_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/design_repo_saver_spec.rb'
+- './spec/lib/gitlab/import_export/duration_measuring_spec.rb'
+- './spec/lib/gitlab/import_export/error_spec.rb'
+- './spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb'
+- './spec/lib/gitlab/import_export/file_importer_spec.rb'
+- './spec/lib/gitlab/import_export/fork_spec.rb'
+- './spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb'
+- './spec/lib/gitlab/import_export/group/object_builder_spec.rb'
+- './spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
+- './spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/group/tree_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/group/tree_saver_spec.rb'
+- './spec/lib/gitlab/import_export/hash_util_spec.rb'
+- './spec/lib/gitlab/import_export/importer_spec.rb'
+- './spec/lib/gitlab/import_export/import_export_equivalence_spec.rb'
+- './spec/lib/gitlab/import_export/import_export_spec.rb'
+- './spec/lib/gitlab/import_export/import_failure_service_spec.rb'
+- './spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
+- './spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb'
+- './spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb'
+- './spec/lib/gitlab/import_export/json/legacy_writer_spec.rb'
+- './spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb'
+- './spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb'
+- './spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
+- './spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb'
+- './spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/lfs_saver_spec.rb'
+- './spec/lib/gitlab/import_export/log_util_spec.rb'
+- './spec/lib/gitlab/import_export/members_mapper_spec.rb'
+- './spec/lib/gitlab/import_export/merge_request_parser_spec.rb'
+- './spec/lib/gitlab/import_export/model_configuration_spec.rb'
+- './spec/lib/gitlab/import_export/project/export_task_spec.rb'
+- './spec/lib/gitlab/import_export/project/import_task_spec.rb'
+- './spec/lib/gitlab/import_export/project/object_builder_spec.rb'
+- './spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
+- './spec/lib/gitlab/import_export/project/relation_saver_spec.rb'
+- './spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb'
+- './spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb'
+- './spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/project/tree_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
+- './spec/lib/gitlab/import_export/reader_spec.rb'
+- './spec/lib/gitlab/import_export/references_configuration_spec.rb'
+- './spec/lib/gitlab/import_export/remote_stream_upload_spec.rb'
+- './spec/lib/gitlab/import_export/repo_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/repo_saver_spec.rb'
+- './spec/lib/gitlab/import_export/saver_spec.rb'
+- './spec/lib/gitlab/import_export/shared_spec.rb'
+- './spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb'
+- './spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb'
+- './spec/lib/gitlab/import_export/uploads_manager_spec.rb'
+- './spec/lib/gitlab/import_export/uploads_restorer_spec.rb'
+- './spec/lib/gitlab/import_export/uploads_saver_spec.rb'
+- './spec/lib/gitlab/import_export/version_checker_spec.rb'
+- './spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb'
+- './spec/lib/gitlab/import_formatter_spec.rb'
+- './spec/lib/gitlab/import/import_failure_service_spec.rb'
+- './spec/lib/gitlab/import/logger_spec.rb'
+- './spec/lib/gitlab/import/merge_request_creator_spec.rb'
+- './spec/lib/gitlab/import/merge_request_helpers_spec.rb'
+- './spec/lib/gitlab/import/metrics_spec.rb'
+- './spec/lib/gitlab/import/set_async_jid_spec.rb'
+- './spec/lib/gitlab/import_sources_spec.rb'
+- './spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb'
+- './spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb'
+- './spec/lib/gitlab/incoming_email_spec.rb'
+- './spec/lib/gitlab/insecure_key_fingerprint_spec.rb'
+- './spec/lib/gitlab/instrumentation_helper_spec.rb'
+- './spec/lib/gitlab/instrumentation/rate_limiting_gates_spec.rb'
+- './spec/lib/gitlab/instrumentation/redis_base_spec.rb'
+- './spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb'
+- './spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb'
+- './spec/lib/gitlab/instrumentation/redis_spec.rb'
+- './spec/lib/gitlab/internal_post_receive/response_spec.rb'
+- './spec/lib/gitlab/issuable/clone/attributes_rewriter_spec.rb'
+- './spec/lib/gitlab/issuable/clone/copy_resource_events_service_spec.rb'
+- './spec/lib/gitlab/issuable_metadata_spec.rb'
+- './spec/lib/gitlab/issuables_count_for_state_spec.rb'
+- './spec/lib/gitlab/issuable_sorter_spec.rb'
+- './spec/lib/gitlab/issues/rebalancing/state_spec.rb'
+- './spec/lib/gitlab/jira/dvcs_spec.rb'
+- './spec/lib/gitlab/jira_import/base_importer_spec.rb'
+- './spec/lib/gitlab/jira_import/handle_labels_service_spec.rb'
+- './spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
+- './spec/lib/gitlab/jira_import/issues_importer_spec.rb'
+- './spec/lib/gitlab/jira_import/labels_importer_spec.rb'
+- './spec/lib/gitlab/jira_import/metadata_collector_spec.rb'
+- './spec/lib/gitlab/jira_import_spec.rb'
+- './spec/lib/gitlab/jira/middleware_spec.rb'
+- './spec/lib/gitlab/job_waiter_spec.rb'
+- './spec/lib/gitlab/json_cache_spec.rb'
+- './spec/lib/gitlab/json_logger_spec.rb'
+- './spec/lib/gitlab/json_spec.rb'
+- './spec/lib/gitlab/jwt_authenticatable_spec.rb'
+- './spec/lib/gitlab/jwt_token_spec.rb'
+- './spec/lib/gitlab/kas/client_spec.rb'
+- './spec/lib/gitlab/kas_spec.rb'
+- './spec/lib/gitlab/kroki_spec.rb'
+- './spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb'
+- './spec/lib/gitlab/kubernetes/config_maps/aws_node_auth_spec.rb'
+- './spec/lib/gitlab/kubernetes/config_map_spec.rb'
+- './spec/lib/gitlab/kubernetes/default_namespace_spec.rb'
+- './spec/lib/gitlab/kubernetes/deployment_spec.rb'
+- './spec/lib/gitlab/kubernetes/generic_secret_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/api_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/pod_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/base_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/certificate_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/delete_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/init_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/install_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/patch_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v3/base_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v3/delete_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v3/install_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/helm/v3/patch_command_spec.rb'
+- './spec/lib/gitlab/kubernetes/ingress_spec.rb'
+- './spec/lib/gitlab/kubernetes/kube_client_spec.rb'
+- './spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb'
+- './spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb'
+- './spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb'
+- './spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb'
+- './spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb'
+- './spec/lib/gitlab/kubernetes/namespace_spec.rb'
+- './spec/lib/gitlab/kubernetes/node_spec.rb'
+- './spec/lib/gitlab/kubernetes/pod_cmd_spec.rb'
+- './spec/lib/gitlab/kubernetes/role_binding_spec.rb'
+- './spec/lib/gitlab/kubernetes/role_spec.rb'
+- './spec/lib/gitlab/kubernetes/rollout_instances_spec.rb'
+- './spec/lib/gitlab/kubernetes/rollout_status_spec.rb'
+- './spec/lib/gitlab/kubernetes/service_account_spec.rb'
+- './spec/lib/gitlab/kubernetes/service_account_token_spec.rb'
+- './spec/lib/gitlab/kubernetes_spec.rb'
+- './spec/lib/gitlab/kubernetes/tls_secret_spec.rb'
+- './spec/lib/gitlab/language_data_spec.rb'
+- './spec/lib/gitlab/language_detection_spec.rb'
+- './spec/lib/gitlab/lazy_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/client_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/importer_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb'
+- './spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb'
+- './spec/lib/gitlab/lets_encrypt/challenge_spec.rb'
+- './spec/lib/gitlab/lets_encrypt/client_spec.rb'
+- './spec/lib/gitlab/lets_encrypt/order_spec.rb'
+- './spec/lib/gitlab/lets_encrypt_spec.rb'
+- './spec/lib/gitlab/lfs/client_spec.rb'
+- './spec/lib/gitlab/lfs_token_spec.rb'
+- './spec/lib/gitlab/local_and_remote_storage_migration/artifact_migrater_spec.rb'
+- './spec/lib/gitlab/local_and_remote_storage_migration/pages_deployment_migrater_spec.rb'
+- './spec/lib/gitlab/logger_spec.rb'
+- './spec/lib/gitlab/logging/cloudflare_helper_spec.rb'
+- './spec/lib/gitlab/lograge/custom_options_spec.rb'
+- './spec/lib/gitlab/log_timestamp_formatter_spec.rb'
+- './spec/lib/gitlab/loop_helpers_spec.rb'
+- './spec/lib/gitlab/mailgun/webhook_processors/failure_logger_spec.rb'
+- './spec/lib/gitlab/mailgun/webhook_processors/member_invites_spec.rb'
+- './spec/lib/gitlab/mail_room/authenticator_spec.rb'
+- './spec/lib/gitlab/mail_room/mail_room_spec.rb'
+- './spec/lib/gitlab/manifest_import/manifest_spec.rb'
+- './spec/lib/gitlab/manifest_import/metadata_spec.rb'
+- './spec/lib/gitlab/manifest_import/project_creator_spec.rb'
+- './spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb'
+- './spec/lib/gitlab/markdown_cache/field_data_spec.rb'
+- './spec/lib/gitlab/markdown_cache/redis/extension_spec.rb'
+- './spec/lib/gitlab/markdown_cache/redis/store_spec.rb'
+- './spec/lib/gitlab/marker_range_spec.rb'
+- './spec/lib/gitlab/markup_helper_spec.rb'
+- './spec/lib/gitlab/memory/instrumentation_spec.rb'
+- './spec/lib/gitlab/memory/jemalloc_spec.rb'
+- './spec/lib/gitlab/memory/reports_daemon_spec.rb'
+- './spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb'
+- './spec/lib/gitlab/memory/watchdog_spec.rb'
+- './spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb'
+- './spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb'
+- './spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb'
+- './spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb'
+- './spec/lib/gitlab/metrics/background_transaction_spec.rb'
+- './spec/lib/gitlab/metrics/boot_time_tracker_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/cache_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/defaults_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/importer_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/stages/url_validator_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/url_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb'
+- './spec/lib/gitlab/metrics/dashboard/validator_spec.rb'
+- './spec/lib/gitlab/metrics/delta_spec.rb'
+- './spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb'
+- './spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb'
+- './spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb'
+- './spec/lib/gitlab/metrics/exporter/metrics_middleware_spec.rb'
+- './spec/lib/gitlab/metrics/memory_spec.rb'
+- './spec/lib/gitlab/metrics/method_call_spec.rb'
+- './spec/lib/gitlab/metrics/methods_spec.rb'
+- './spec/lib/gitlab/metrics/prometheus_spec.rb'
+- './spec/lib/gitlab/metrics/rack_middleware_spec.rb'
+- './spec/lib/gitlab/metrics/rails_slis_spec.rb'
+- './spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb'
+- './spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb'
+- './spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb'
+- './spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb'
+- './spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb'
+- './spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb'
+- './spec/lib/gitlab/metrics/sli_spec.rb'
+- './spec/lib/gitlab/metrics_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/action_view_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/active_record_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/external_http_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb'
+- './spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb'
+- './spec/lib/gitlab/metrics/system_spec.rb'
+- './spec/lib/gitlab/metrics/transaction_spec.rb'
+- './spec/lib/gitlab/metrics/web_transaction_spec.rb'
+- './spec/lib/gitlab/middleware/basic_health_check_spec.rb'
+- './spec/lib/gitlab/middleware/compressed_json_spec.rb'
+- './spec/lib/gitlab/middleware/go_spec.rb'
+- './spec/lib/gitlab/middleware/handle_ip_spoof_attack_error_spec.rb'
+- './spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb'
+- './spec/lib/gitlab/middleware/memory_report_spec.rb'
+- './spec/lib/gitlab/middleware/multipart/handler_spec.rb'
+- './spec/lib/gitlab/middleware/multipart_spec.rb'
+- './spec/lib/gitlab/middleware/query_analyzer_spec.rb'
+- './spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb'
+- './spec/lib/gitlab/middleware/rails_queue_duration_spec.rb'
+- './spec/lib/gitlab/middleware/read_only_spec.rb'
+- './spec/lib/gitlab/middleware/release_env_spec.rb'
+- './spec/lib/gitlab/middleware/request_context_spec.rb'
+- './spec/lib/gitlab/middleware/same_site_cookies_spec.rb'
+- './spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb'
+- './spec/lib/gitlab/middleware/speedscope_spec.rb'
+- './spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb'
+- './spec/lib/gitlab/monitor/demo_projects_spec.rb'
+- './spec/lib/gitlab/multi_collection_paginator_spec.rb'
+- './spec/lib/gitlab/multi_destination_logger_spec.rb'
+- './spec/lib/gitlab/namespaced_session_store_spec.rb'
+- './spec/lib/gitlab/nav/top_nav_menu_item_spec.rb'
+- './spec/lib/gitlab/net_http_adapter_spec.rb'
+- './spec/lib/gitlab/no_cache_headers_spec.rb'
+- './spec/lib/gitlab/noteable_metadata_spec.rb'
+- './spec/lib/gitlab/null_request_store_spec.rb'
+- './spec/lib/gitlab/object_hierarchy_spec.rb'
+- './spec/lib/gitlab/octokit/middleware_spec.rb'
+- './spec/lib/gitlab/omniauth_initializer_spec.rb'
+- './spec/lib/gitlab/optimistic_locking_spec.rb'
+- './spec/lib/gitlab/other_markup_spec.rb'
+- './spec/lib/gitlab/otp_key_rotator_spec.rb'
+- './spec/lib/gitlab/pages/cache_control_spec.rb'
+- './spec/lib/gitlab/pages/deployment_update_spec.rb'
+- './spec/lib/gitlab/pages/settings_spec.rb'
+- './spec/lib/gitlab/pages_spec.rb'
+- './spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb'
+- './spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/array_scope_columns_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/column_data_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/iterator_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/order_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/pager_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/page_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/paginator_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/request_context_spec.rb'
+- './spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb'
+- './spec/lib/gitlab/pagination/keyset_spec.rb'
+- './spec/lib/gitlab/pagination/offset_header_builder_spec.rb'
+- './spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb'
+- './spec/lib/gitlab/pagination/offset_pagination_spec.rb'
+- './spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb'
+- './spec/lib/gitlab/patch/database_config_spec.rb'
+- './spec/lib/gitlab/patch/draw_route_spec.rb'
+- './spec/lib/gitlab/patch/prependable_spec.rb'
+- './spec/lib/gitlab/path_regex_spec.rb'
+- './spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb'
+- './spec/lib/gitlab/performance_bar_spec.rb'
+- './spec/lib/gitlab/performance_bar/stats_spec.rb'
+- './spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb'
+- './spec/lib/gitlab/phabricator_import/cache/map_spec.rb'
+- './spec/lib/gitlab/phabricator_import/conduit/client_spec.rb'
+- './spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb'
+- './spec/lib/gitlab/phabricator_import/conduit/response_spec.rb'
+- './spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb'
+- './spec/lib/gitlab/phabricator_import/conduit/user_spec.rb'
+- './spec/lib/gitlab/phabricator_import/conduit/users_response_spec.rb'
+- './spec/lib/gitlab/phabricator_import/importer_spec.rb'
+- './spec/lib/gitlab/phabricator_import/issues/importer_spec.rb'
+- './spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb'
+- './spec/lib/gitlab/phabricator_import/project_creator_spec.rb'
+- './spec/lib/gitlab/phabricator_import/representation/task_spec.rb'
+- './spec/lib/gitlab/phabricator_import/representation/user_spec.rb'
+- './spec/lib/gitlab/phabricator_import/user_finder_spec.rb'
+- './spec/lib/gitlab/phabricator_import/worker_state_spec.rb'
+- './spec/lib/gitlab/pipeline_scope_counts_spec.rb'
+- './spec/lib/gitlab/polling_interval_spec.rb'
+- './spec/lib/gitlab/popen/runner_spec.rb'
+- './spec/lib/gitlab/popen_spec.rb'
+- './spec/lib/gitlab/private_commit_email_spec.rb'
+- './spec/lib/gitlab/process_management_spec.rb'
+- './spec/lib/gitlab/process_memory_cache/helper_spec.rb'
+- './spec/lib/gitlab/process_supervisor_spec.rb'
+- './spec/lib/gitlab/profiler_spec.rb'
+- './spec/lib/gitlab/project_authorizations_spec.rb'
+- './spec/lib/gitlab/project_search_results_spec.rb'
+- './spec/lib/gitlab/project_stats_refresh_conflicts_logger_spec.rb'
+- './spec/lib/gitlab/project_template_spec.rb'
+- './spec/lib/gitlab/project_transfer_spec.rb'
+- './spec/lib/gitlab/prometheus/adapter_spec.rb'
+- './spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
+- './spec/lib/gitlab/prometheus_client_spec.rb'
+- './spec/lib/gitlab/prometheus/internal_spec.rb'
+- './spec/lib/gitlab/prometheus/metric_group_spec.rb'
+- './spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
+- './spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
+- './spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
+- './spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb'
+- './spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
+- './spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
+- './spec/lib/gitlab/prometheus/query_variables_spec.rb'
+- './spec/lib/gitlab/protocol_access_spec.rb'
+- './spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
+- './spec/lib/gitlab/push_options_spec.rb'
+- './spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb'
+- './spec/lib/gitlab/query_limiting/middleware_spec.rb'
+- './spec/lib/gitlab/query_limiting_spec.rb'
+- './spec/lib/gitlab/query_limiting/transaction_spec.rb'
+- './spec/lib/gitlab/quick_actions/command_definition_spec.rb'
+- './spec/lib/gitlab/quick_actions/dsl_spec.rb'
+- './spec/lib/gitlab/quick_actions/extractor_spec.rb'
+- './spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb'
+- './spec/lib/gitlab/quick_actions/substitution_definition_spec.rb'
+- './spec/lib/gitlab/quick_actions/users_extractor_spec.rb'
+- './spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb'
+- './spec/lib/gitlab/rack_attack/request_spec.rb'
+- './spec/lib/gitlab/rack_attack_spec.rb'
+- './spec/lib/gitlab/rack_attack/user_allowlist_spec.rb'
+- './spec/lib/gitlab/reactive_cache_set_cache_spec.rb'
+- './spec/lib/gitlab/redis/boolean_spec.rb'
+- './spec/lib/gitlab/redis/cache_spec.rb'
+- './spec/lib/gitlab/redis/duplicate_jobs_spec.rb'
+- './spec/lib/gitlab/redis/hll_spec.rb'
+- './spec/lib/gitlab/redis/multi_store_spec.rb'
+- './spec/lib/gitlab/redis/queues_spec.rb'
+- './spec/lib/gitlab/redis/rate_limiting_spec.rb'
+- './spec/lib/gitlab/redis/sessions_spec.rb'
+- './spec/lib/gitlab/redis/shared_state_spec.rb'
+- './spec/lib/gitlab/redis/sidekiq_status_spec.rb'
+- './spec/lib/gitlab/redis/trace_chunks_spec.rb'
+- './spec/lib/gitlab/redis/wrapper_spec.rb'
+- './spec/lib/gitlab/reference_counter_spec.rb'
+- './spec/lib/gitlab/reference_extractor_spec.rb'
+- './spec/lib/gitlab/regex_requires_app_spec.rb'
+- './spec/lib/gitlab/regex_spec.rb'
+- './spec/lib/gitlab/relative_positioning/item_context_spec.rb'
+- './spec/lib/gitlab/relative_positioning/mover_spec.rb'
+- './spec/lib/gitlab/relative_positioning/range_spec.rb'
+- './spec/lib/gitlab/render_timeout_spec.rb'
+- './spec/lib/gitlab/repo_path_spec.rb'
+- './spec/lib/gitlab/repository_archive_rate_limiter_spec.rb'
+- './spec/lib/gitlab/repository_cache_adapter_spec.rb'
+- './spec/lib/gitlab/repository_cache/preloader_spec.rb'
+- './spec/lib/gitlab/repository_cache_spec.rb'
+- './spec/lib/gitlab/repository_hash_cache_spec.rb'
+- './spec/lib/gitlab/repository_set_cache_spec.rb'
+- './spec/lib/gitlab/repository_size_checker_spec.rb'
+- './spec/lib/gitlab/repository_size_error_message_spec.rb'
+- './spec/lib/gitlab/repository_url_builder_spec.rb'
+- './spec/lib/gitlab/request_context_spec.rb'
+- './spec/lib/gitlab/request_endpoints_spec.rb'
+- './spec/lib/gitlab/request_forgery_protection_spec.rb'
+- './spec/lib/gitlab/robots_txt/parser_spec.rb'
+- './spec/lib/gitlab/route_map_spec.rb'
+- './spec/lib/gitlab/routing_spec.rb'
+- './spec/lib/gitlab/rugged_instrumentation_spec.rb'
+- './spec/lib/gitlab/runtime_spec.rb'
+- './spec/lib/gitlab/saas_spec.rb'
+- './spec/lib/gitlab/safe_request_loader_spec.rb'
+- './spec/lib/gitlab/safe_request_purger_spec.rb'
+- './spec/lib/gitlab/safe_request_store_spec.rb'
+- './spec/lib/gitlab/sample_data_template_spec.rb'
+- './spec/lib/gitlab/sanitizers/exception_message_spec.rb'
+- './spec/lib/gitlab/sanitizers/exif_spec.rb'
+- './spec/lib/gitlab/sanitizers/svg_spec.rb'
+- './spec/lib/gitlab/search/abuse_detection_spec.rb'
+- './spec/lib/gitlab/search/abuse_validators/no_abusive_coercion_from_string_validator_spec.rb'
+- './spec/lib/gitlab/search/abuse_validators/no_abusive_term_length_validator_spec.rb'
+- './spec/lib/gitlab/search_context/builder_spec.rb'
+- './spec/lib/gitlab/search_context/controller_concern_spec.rb'
+- './spec/lib/gitlab/search/found_blob_spec.rb'
+- './spec/lib/gitlab/search/found_wiki_page_spec.rb'
+- './spec/lib/gitlab/search/params_spec.rb'
+- './spec/lib/gitlab/search/query_spec.rb'
+- './spec/lib/gitlab/search/recent_issues_spec.rb'
+- './spec/lib/gitlab/search/recent_merge_requests_spec.rb'
+- './spec/lib/gitlab/search_results_spec.rb'
+- './spec/lib/gitlab/search/sort_options_spec.rb'
+- './spec/lib/gitlab/security/scan_configuration_spec.rb'
+- './spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb'
+- './spec/lib/gitlab/seeder_spec.rb'
+- './spec/lib/gitlab/serializer/ci/variables_spec.rb'
+- './spec/lib/gitlab/serializer/pagination_spec.rb'
+- './spec/lib/gitlab/serverless/service_spec.rb'
+- './spec/lib/gitlab/service_desk_email_spec.rb'
+- './spec/lib/gitlab/service_desk_spec.rb'
+- './spec/lib/gitlab/session_spec.rb'
+- './spec/lib/gitlab/setup_helper/praefect_spec.rb'
+- './spec/lib/gitlab/setup_helper/workhorse_spec.rb'
+- './spec/lib/gitlab/shard_health_cache_spec.rb'
+- './spec/lib/gitlab/shell_spec.rb'
+- './spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb'
+- './spec/lib/gitlab/sidekiq_config_spec.rb'
+- './spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb'
+- './spec/lib/gitlab/sidekiq_config/worker_router_spec.rb'
+- './spec/lib/gitlab/sidekiq_config/worker_spec.rb'
+- './spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb'
+- './spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb'
+- './spec/lib/gitlab/sidekiq_death_handler_spec.rb'
+- './spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb'
+- './spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb'
+- './spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/size_limiter/client_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/size_limiter/compressor_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/size_limiter/exceed_limit_error_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb'
+- './spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb'
+- './spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb'
+- './spec/lib/gitlab/sidekiq_queue_spec.rb'
+- './spec/lib/gitlab/sidekiq_signals_spec.rb'
+- './spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb'
+- './spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb'
+- './spec/lib/gitlab/sidekiq_status_spec.rb'
+- './spec/lib/gitlab/sidekiq_versioning/middleware_spec.rb'
+- './spec/lib/gitlab/sidekiq_versioning_spec.rb'
+- './spec/lib/gitlab/sidekiq_versioning/worker_spec.rb'
+- './spec/lib/gitlab/slash_commands/application_help_spec.rb'
+- './spec/lib/gitlab/slash_commands/command_spec.rb'
+- './spec/lib/gitlab/slash_commands/deploy_spec.rb'
+- './spec/lib/gitlab/slash_commands/issue_close_spec.rb'
+- './spec/lib/gitlab/slash_commands/issue_comment_spec.rb'
+- './spec/lib/gitlab/slash_commands/issue_move_spec.rb'
+- './spec/lib/gitlab/slash_commands/issue_new_spec.rb'
+- './spec/lib/gitlab/slash_commands/issue_search_spec.rb'
+- './spec/lib/gitlab/slash_commands/issue_show_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/access_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/error_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb'
+- './spec/lib/gitlab/slash_commands/presenters/run_spec.rb'
+- './spec/lib/gitlab/slash_commands/run_spec.rb'
+- './spec/lib/gitlab/slug/environment_spec.rb'
+- './spec/lib/gitlab/snippet_search_results_spec.rb'
+- './spec/lib/gitlab/sourcegraph_spec.rb'
+- './spec/lib/gitlab/spamcheck/client_spec.rb'
+- './spec/lib/gitlab_spec.rb'
+- './spec/lib/gitlab/sql/cte_spec.rb'
+- './spec/lib/gitlab/sql/except_spec.rb'
+- './spec/lib/gitlab/sql/glob_spec.rb'
+- './spec/lib/gitlab/sql/intersect_spec.rb'
+- './spec/lib/gitlab/sql/pattern_spec.rb'
+- './spec/lib/gitlab/sql/recursive_cte_spec.rb'
+- './spec/lib/gitlab/sql/union_spec.rb'
+- './spec/lib/gitlab/ssh/commit_spec.rb'
+- './spec/lib/gitlab/ssh_public_key_spec.rb'
+- './spec/lib/gitlab/ssh/signature_spec.rb'
+- './spec/lib/gitlab/string_placeholder_replacer_spec.rb'
+- './spec/lib/gitlab/string_range_marker_spec.rb'
+- './spec/lib/gitlab/string_regex_marker_spec.rb'
+- './spec/lib/gitlab/submodule_links_spec.rb'
+- './spec/lib/gitlab/subscription_portal_spec.rb'
+- './spec/lib/gitlab/suggestions/commit_message_spec.rb'
+- './spec/lib/gitlab/suggestions/file_suggestion_spec.rb'
+- './spec/lib/gitlab/suggestions/suggestion_set_spec.rb'
+- './spec/lib/gitlab/tab_width_spec.rb'
+- './spec/lib/gitlab/tcp_checker_spec.rb'
+- './spec/lib/gitlab/template/finders/global_template_finder_spec.rb'
+- './spec/lib/gitlab/template/finders/repo_template_finders_spec.rb'
+- './spec/lib/gitlab/template/gitignore_template_spec.rb'
+- './spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb'
+- './spec/lib/gitlab/template/issue_template_spec.rb'
+- './spec/lib/gitlab/template/merge_request_template_spec.rb'
+- './spec/lib/gitlab/template/metrics_dashboard_template_spec.rb'
+- './spec/lib/gitlab/template_parser/ast_spec.rb'
+- './spec/lib/gitlab/template_parser/parser_spec.rb'
+- './spec/lib/gitlab/terraform_registry_token_spec.rb'
+- './spec/lib/gitlab/terraform/state_migration_helper_spec.rb'
+- './spec/lib/gitlab/themes_spec.rb'
+- './spec/lib/gitlab/throttle_spec.rb'
+- './spec/lib/gitlab/time_tracking_formatter_spec.rb'
+- './spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb'
+- './spec/lib/gitlab/tracking/destinations/snowplow_spec.rb'
+- './spec/lib/gitlab/tracking/event_definition_spec.rb'
+- './spec/lib/gitlab/tracking/incident_management_spec.rb'
+- './spec/lib/gitlab/tracking/snowplow_schema_validation_spec.rb'
+- './spec/lib/gitlab/tracking_spec.rb'
+- './spec/lib/gitlab/tracking/standard_context_spec.rb'
+- './spec/lib/gitlab/tree_summary_spec.rb'
+- './spec/lib/gitlab/unicode_spec.rb'
+- './spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb'
+- './spec/lib/gitlab/untrusted_regexp_spec.rb'
+- './spec/lib/gitlab/uploads_transfer_spec.rb'
+- './spec/lib/gitlab/url_blockers/domain_allowlist_entry_spec.rb'
+- './spec/lib/gitlab/url_blockers/ip_allowlist_entry_spec.rb'
+- './spec/lib/gitlab/url_blocker_spec.rb'
+- './spec/lib/gitlab/url_blockers/url_allowlist_spec.rb'
+- './spec/lib/gitlab/url_builder_spec.rb'
+- './spec/lib/gitlab/url_sanitizer_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/base_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/ipynb_diff_activity_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/jetbrains_plugin_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/merge_request_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/note_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/productivity_analytics_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/search_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/service_usage_data_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/snippet_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/vscode_extension_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/wiki_page_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb'
+- './spec/lib/gitlab/usage_data_metrics_spec.rb'
+- './spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb'
+- './spec/lib/gitlab/usage_data_queries_spec.rb'
+- './spec/lib/gitlab/usage_data_spec.rb'
+- './spec/lib/gitlab/usage_data/topology_spec.rb'
+- './spec/lib/gitlab/usage/metric_definition_spec.rb'
+- './spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb'
+- './spec/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection_spec.rb'
+- './spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb'
+- './spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/active_user_count_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/cert_based_clusters_ff_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_users_associating_milestones_to_releases_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/hostname_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/service_ping_features_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/snowplow_configured_to_gitlab_collector_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/snowplow_enabled_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/key_path_processor_spec.rb'
+- './spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb'
+- './spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb'
+- './spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb'
+- './spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb'
+- './spec/lib/gitlab/usage/metric_spec.rb'
+- './spec/lib/gitlab/usage/metrics/query_spec.rb'
+- './spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb'
+- './spec/lib/gitlab/usage/service_ping/legacy_metric_timing_decorator_spec.rb'
+- './spec/lib/gitlab/usage/service_ping/payload_keys_processor_spec.rb'
+- './spec/lib/gitlab/usage/service_ping_report_spec.rb'
+- './spec/lib/gitlab/user_access_snippet_spec.rb'
+- './spec/lib/gitlab/user_access_spec.rb'
+- './spec/lib/gitlab/uuid_spec.rb'
+- './spec/lib/gitlab/verify/job_artifacts_spec.rb'
+- './spec/lib/gitlab/verify/lfs_objects_spec.rb'
+- './spec/lib/gitlab/verify/uploads_spec.rb'
+- './spec/lib/gitlab/version_info_spec.rb'
+- './spec/lib/gitlab/view/presenter/base_spec.rb'
+- './spec/lib/gitlab/view/presenter/delegated_spec.rb'
+- './spec/lib/gitlab/view/presenter/factory_spec.rb'
+- './spec/lib/gitlab/view/presenter/simple_spec.rb'
+- './spec/lib/gitlab/visibility_level_checker_spec.rb'
+- './spec/lib/gitlab/visibility_level_spec.rb'
+- './spec/lib/gitlab/web_hooks/rate_limiter_spec.rb'
+- './spec/lib/gitlab/web_hooks/recursion_detection_spec.rb'
+- './spec/lib/gitlab/web_ide/config/entry/global_spec.rb'
+- './spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb'
+- './spec/lib/gitlab/web_ide/config_spec.rb'
+- './spec/lib/gitlab/webpack/file_loader_spec.rb'
+- './spec/lib/gitlab/webpack/graphql_known_operations_spec.rb'
+- './spec/lib/gitlab/webpack/manifest_spec.rb'
+- './spec/lib/gitlab/wiki_file_finder_spec.rb'
+- './spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb'
+- './spec/lib/gitlab/with_request_store_spec.rb'
+- './spec/lib/gitlab/word_diff/chunk_collection_spec.rb'
+- './spec/lib/gitlab/word_diff/line_processor_spec.rb'
+- './spec/lib/gitlab/word_diff/parser_spec.rb'
+- './spec/lib/gitlab/word_diff/positions_counter_spec.rb'
+- './spec/lib/gitlab/word_diff/segments/chunk_spec.rb'
+- './spec/lib/gitlab/word_diff/segments/diff_hunk_spec.rb'
+- './spec/lib/gitlab/word_diff/segments/newline_spec.rb'
+- './spec/lib/gitlab/workhorse_spec.rb'
+- './spec/lib/gitlab/x509/certificate_spec.rb'
+- './spec/lib/gitlab/x509/commit_spec.rb'
+- './spec/lib/gitlab/x509/signature_spec.rb'
+- './spec/lib/gitlab/x509/tag_spec.rb'
+- './spec/lib/gitlab/zentao/client_spec.rb'
+- './spec/lib/gitlab/zentao/query_spec.rb'
+- './spec/lib/gitlab/zoom_link_extractor_spec.rb'
+- './spec/lib/google_api/auth_spec.rb'
+- './spec/lib/google_api/cloud_platform/client_spec.rb'
+- './spec/lib/grafana/client_spec.rb'
+- './spec/lib/grafana/time_window_spec.rb'
+- './spec/lib/grafana/validator_spec.rb'
+- './spec/lib/initializer_connections_spec.rb'
+- './spec/lib/json_web_token/hmac_token_spec.rb'
+- './spec/lib/json_web_token/rsa_token_spec.rb'
+- './spec/lib/json_web_token/token_spec.rb'
+- './spec/lib/kramdown/kramdown_spec.rb'
+- './spec/lib/kramdown/parser/atlassian_document_format_spec.rb'
+- './spec/lib/marginalia_spec.rb'
+- './spec/lib/mattermost/client_spec.rb'
+- './spec/lib/mattermost/command_spec.rb'
+- './spec/lib/mattermost/session_spec.rb'
+- './spec/lib/mattermost/team_spec.rb'
+- './spec/lib/microsoft_teams/activity_spec.rb'
+- './spec/lib/microsoft_teams/notifier_spec.rb'
+- './spec/lib/object_storage/config_spec.rb'
+- './spec/lib/object_storage/direct_upload_spec.rb'
+- './spec/lib/omni_auth/strategies/jwt_spec.rb'
+- './spec/lib/pager_duty/webhook_payload_parser_spec.rb'
+- './spec/lib/peek/views/active_record_spec.rb'
+- './spec/lib/peek/views/bullet_detailed_spec.rb'
+- './spec/lib/peek/views/detailed_view_spec.rb'
+- './spec/lib/peek/views/external_http_spec.rb'
+- './spec/lib/peek/views/memory_spec.rb'
+- './spec/lib/peek/views/redis_detailed_spec.rb'
+- './spec/lib/peek/views/rugged_spec.rb'
+- './spec/lib/product_analytics/event_params_spec.rb'
+- './spec/lib/product_analytics/tracker_spec.rb'
+- './spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb'
+- './spec/lib/prometheus/pid_provider_spec.rb'
+- './spec/lib/quality/seeders/issues_spec.rb'
+- './spec/lib/release_highlights/validator/entry_spec.rb'
+- './spec/lib/release_highlights/validator_spec.rb'
+- './spec/lib/rouge/formatters/html_gitlab_spec.rb'
+- './spec/lib/safe_zip/entry_spec.rb'
+- './spec/lib/safe_zip/extract_params_spec.rb'
+- './spec/lib/safe_zip/extract_spec.rb'
+- './spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb'
+- './spec/lib/security/ci_configuration/sast_build_action_spec.rb'
+- './spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb'
+- './spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb'
+- './spec/lib/security/report_schema_version_matcher_spec.rb'
+- './spec/lib/serializers/json_spec.rb'
+- './spec/lib/serializers/symbolized_json_spec.rb'
+- './spec/lib/serializers/unsafe_json_spec.rb'
+- './spec/lib/service_ping/build_payload_spec.rb'
+- './spec/lib/service_ping/devops_report_spec.rb'
+- './spec/lib/service_ping/permit_data_categories_spec.rb'
+- './spec/lib/service_ping/service_ping_settings_spec.rb'
+- './spec/lib/sidebars/concerns/container_with_html_options_spec.rb'
+- './spec/lib/sidebars/concerns/link_with_html_options_spec.rb'
+- './spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/group_information_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/issues_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/scope_menu_spec.rb'
+- './spec/lib/sidebars/groups/menus/settings_menu_spec.rb'
+- './spec/lib/sidebars/menu_item_spec.rb'
+- './spec/lib/sidebars/menu_spec.rb'
+- './spec/lib/sidebars/panel_spec.rb'
+- './spec/lib/sidebars/projects/context_spec.rb'
+- './spec/lib/sidebars/projects/menus/analytics_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/confluence_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/deployments_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/hidden_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/issues_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/monitor_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/project_information_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/repository_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/scope_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/settings_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/shimo_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/snippets_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/wiki_menu_spec.rb'
+- './spec/lib/sidebars/projects/menus/zentao_menu_spec.rb'
+- './spec/lib/sidebars/projects/panel_spec.rb'
+- './spec/lib/system_check/app/authorized_keys_permission_check_spec.rb'
+- './spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb'
+- './spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb'
+- './spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb'
+- './spec/lib/system_check/base_check_spec.rb'
+- './spec/lib/system_check/incoming_email_check_spec.rb'
+- './spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb'
+- './spec/lib/system_check/orphans/namespace_check_spec.rb'
+- './spec/lib/system_check/orphans/repository_check_spec.rb'
+- './spec/lib/system_check/sidekiq_check_spec.rb'
+- './spec/lib/system_check/simple_executor_spec.rb'
+- './spec/lib/system_check_spec.rb'
+- './spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb'
+- './spec/lib/unnested_in_filters/dsl_spec.rb'
+- './spec/lib/unnested_in_filters/rewriter_spec.rb'
+- './spec/lib/uploaded_file_spec.rb'
+- './spec/lib/version_check_spec.rb'
+- './spec/mailers/abuse_report_mailer_spec.rb'
+- './spec/mailers/devise_mailer_spec.rb'
+- './spec/mailers/email_rejection_mailer_spec.rb'
+- './spec/mailers/emails/admin_notification_spec.rb'
+- './spec/mailers/emails/auto_devops_spec.rb'
+- './spec/mailers/emails/groups_spec.rb'
+- './spec/mailers/emails/in_product_marketing_spec.rb'
+- './spec/mailers/emails/issues_spec.rb'
+- './spec/mailers/emails/merge_requests_spec.rb'
+- './spec/mailers/emails/pages_domains_spec.rb'
+- './spec/mailers/emails/pipelines_spec.rb'
+- './spec/mailers/emails/profile_spec.rb'
+- './spec/mailers/emails/projects_spec.rb'
+- './spec/mailers/emails/releases_spec.rb'
+- './spec/mailers/emails/service_desk_spec.rb'
+- './spec/mailers/notify_spec.rb'
+- './spec/mailers/repository_check_mailer_spec.rb'
+- './spec/metrics_server/metrics_server_spec.rb'
+- './spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb'
+- './spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb'
+- './spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb'
+- './spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb'
+- './spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb'
+- './spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb'
+- './spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb'
+- './spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb'
+- './spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb'
+- './spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb'
+- './spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb'
+- './spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb'
+- './spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb'
+- './spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb'
+- './spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb'
+- './spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb'
+- './spec/migrations/20210804150320_create_base_work_item_types_spec.rb'
+- './spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb'
+- './spec/migrations/20210811122206_update_external_project_bots_spec.rb'
+- './spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb'
+- './spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb'
+- './spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb'
+- './spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb'
+- './spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb'
+- './spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb'
+- './spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb'
+- './spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb'
+- './spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb'
+- './spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb'
+- './spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb'
+- './spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb'
+- './spec/migrations/20210918201050_remove_old_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb'
+- './spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb'
+- './spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb'
+- './spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb'
+- './spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb'
+- './spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb'
+- './spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb'
+- './spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb'
+- './spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb'
+- './spec/migrations/20211101222614_consume_remaining_user_namespace_jobs_spec.rb'
+- './spec/migrations/20211110143306_add_not_null_constraint_to_security_findings_uuid_spec.rb'
+- './spec/migrations/20211110151350_schedule_drop_invalid_security_findings_spec.rb'
+- './spec/migrations/20211116091751_change_namespace_type_default_to_user_spec.rb'
+- './spec/migrations/20211116111644_schedule_remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
+- './spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb'
+- './spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb'
+- './spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb'
+- './spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb'
+- './spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb'
+- './spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb'
+- './spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb'
+- './spec/migrations/20211210140629_encrypt_static_object_token_spec.rb'
+- './spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb'
+- './spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb'
+- './spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb'
+- './spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb'
+- './spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb'
+- './spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb'
+- './spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb'
+- './spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb'
+- './spec/migrations/20220124130028_dedup_runner_projects_spec.rb'
+- './spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb'
+- './spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb'
+- './spec/migrations/20220202105733_delete_service_template_records_spec.rb'
+- './spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb'
+- './spec/migrations/20220204194347_encrypt_integration_properties_spec.rb'
+- './spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb'
+- './spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb'
+- './spec/migrations/20220213103859_remove_integrations_type_spec.rb'
+- './spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb'
+- './spec/migrations/20220222192525_remove_null_releases_spec.rb'
+- './spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb'
+- './spec/migrations/20220305223212_add_security_training_providers_spec.rb'
+- './spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb'
+- './spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb'
+- './spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb'
+- './spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb'
+- './spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb'
+- './spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb'
+- './spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb'
+- './spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb'
+- './spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb'
+- './spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb'
+- './spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb'
+- './spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb'
+- './spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb'
+- './spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb'
+- './spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb'
+- './spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb'
+- './spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb'
+- './spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb'
+- './spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb'
+- './spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb'
+- './spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb'
+- './spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb'
+- './spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb'
+- './spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb'
+- './spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb'
+- './spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb'
+- './spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb'
+- './spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb'
+- './spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb'
+- './spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb'
+- './spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb'
+- './spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb'
+- './spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb'
+- './spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb'
+- './spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb'
+- './spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb'
+- './spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb'
+- './spec/migrations/20220715163254_update_notes_in_past_spec.rb'
+- './spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
+- './spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
+- './spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
+- './spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
+- './spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb'
+- './spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb'
+- './spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb'
+- './spec/migrations/20220809002011_schedule_destroy_invalid_group_members_spec.rb'
+- './spec/migrations/active_record/schema_spec.rb'
+- './spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb'
+- './spec/migrations/add_epics_relative_position_spec.rb'
+- './spec/migrations/add_new_trail_plans_spec.rb'
+- './spec/migrations/add_open_source_plan_spec.rb'
+- './spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb'
+- './spec/migrations/add_triggers_to_integrations_type_new_spec.rb'
+- './spec/migrations/add_upvotes_count_index_to_issues_spec.rb'
+- './spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb'
+- './spec/migrations/associate_existing_dast_builds_with_variables_spec.rb'
+- './spec/migrations/backfill_all_project_namespaces_spec.rb'
+- './spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb'
+- './spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb'
+- './spec/migrations/backfill_cycle_analytics_aggregations_spec.rb'
+- './spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb'
+- './spec/migrations/backfill_group_features_spec.rb'
+- './spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb'
+- './spec/migrations/backfill_integrations_type_new_spec.rb'
+- './spec/migrations/backfill_issues_upvotes_count_spec.rb'
+- './spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb'
+- './spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb'
+- './spec/migrations/backfill_namespace_id_for_project_routes_spec.rb'
+- './spec/migrations/backfill_nuget_temporary_packages_to_processing_status_spec.rb'
+- './spec/migrations/backfill_project_import_level_spec.rb'
+- './spec/migrations/backfill_project_namespaces_for_group_spec.rb'
+- './spec/migrations/backfill_stage_event_hash_spec.rb'
+- './spec/migrations/backfill_user_namespace_spec.rb'
+- './spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb'
+- './spec/migrations/change_public_projects_cost_factor_spec.rb'
+- './spec/migrations/change_web_hook_events_default_spec.rb'
+- './spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb'
+- './spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb'
+- './spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb'
+- './spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb'
+- './spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb'
+- './spec/migrations/cleanup_mr_attention_request_todos_spec.rb'
+- './spec/migrations/cleanup_orphaned_routes_spec.rb'
+- './spec/migrations/clean_up_pending_builds_table_spec.rb'
+- './spec/migrations/cleanup_remaining_orphan_invites_spec.rb'
+- './spec/migrations/confirm_security_bot_spec.rb'
+- './spec/migrations/confirm_support_bot_user_spec.rb'
+- './spec/migrations/delete_security_findings_without_uuid_spec.rb'
+- './spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb'
+- './spec/migrations/disable_job_token_scope_when_unused_spec.rb'
+- './spec/migrations/finalize_orphaned_routes_cleanup_spec.rb'
+- './spec/migrations/finalize_project_namespaces_backfill_spec.rb'
+- './spec/migrations/finalize_routes_backfilling_for_projects_spec.rb'
+- './spec/migrations/finalize_traversal_ids_background_migrations_spec.rb'
+- './spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb'
+- './spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb'
+- './spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb'
+- './spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb'
+- './spec/migrations/migrate_elastic_index_settings_spec.rb'
+- './spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb'
+- './spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb'
+- './spec/migrations/orphaned_invite_tokens_cleanup_spec.rb'
+- './spec/migrations/populate_audit_event_streaming_verification_token_spec.rb'
+- './spec/migrations/populate_dismissal_information_for_vulnerabilities_spec.rb'
+- './spec/migrations/populate_operation_visibility_permissions_spec.rb'
+- './spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb'
+- './spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb'
+- './spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb'
+- './spec/migrations/remove_duplicate_dast_site_tokens_spec.rb'
+- './spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb'
+- './spec/migrations/remove_hipchat_service_records_spec.rb'
+- './spec/migrations/remove_invalid_integrations_spec.rb'
+- './spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb'
+- './spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb'
+- './spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb'
+- './spec/migrations/remove_wiki_notes_spec.rb'
+- './spec/migrations/rename_services_to_integrations_spec.rb'
+- './spec/migrations/replace_external_wiki_triggers_spec.rb'
+- './spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb'
+- './spec/migrations/reschedule_delete_orphaned_deployments_spec.rb'
+- './spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb'
+- './spec/migrations/reset_job_token_scope_enabled_again_spec.rb'
+- './spec/migrations/reset_job_token_scope_enabled_spec.rb'
+- './spec/migrations/reset_severity_levels_to_new_default_spec.rb'
+- './spec/migrations/retry_backfill_traversal_ids_spec.rb'
+- './spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb'
+- './spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb'
+- './spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb'
+- './spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb'
+- './spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb'
+- './spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb'
+- './spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb'
+- './spec/migrations/schedule_migrate_shared_vulnerability_scanners_spec.rb'
+- './spec/migrations/schedule_populate_requirements_issue_id_spec.rb'
+- './spec/migrations/schedule_purging_stale_security_scans_spec.rb'
+- './spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb'
+- './spec/migrations/schedule_security_setting_creation_spec.rb'
+- './spec/migrations/schedule_set_correct_vulnerability_state_spec.rb'
+- './spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb'
+- './spec/migrations/schedule_update_timelogs_project_id_spec.rb'
+- './spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb'
+- './spec/migrations/set_default_job_token_scope_true_spec.rb'
+- './spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb'
+- './spec/migrations/start_backfill_ci_queuing_tables_spec.rb'
+- './spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb'
+- './spec/migrations/toggle_vsa_aggregations_enable_spec.rb'
+- './spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb'
+- './spec/migrations/update_application_settings_protected_paths_spec.rb'
+- './spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb'
+- './spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb'
+- './spec/migrations/update_invalid_member_states_spec.rb'
+- './spec/migrations/update_invalid_web_hooks_spec.rb'
+- './spec/models/ability_spec.rb'
+- './spec/models/abuse_report_spec.rb'
+- './spec/models/active_session_spec.rb'
+- './spec/models/acts_as_taggable_on/tagging_spec.rb'
+- './spec/models/acts_as_taggable_on/tag_spec.rb'
+- './spec/models/alerting/project_alerting_setting_spec.rb'
+- './spec/models/alert_management/alert_assignee_spec.rb'
+- './spec/models/alert_management/alert_spec.rb'
+- './spec/models/alert_management/alert_user_mention_spec.rb'
+- './spec/models/alert_management/http_integration_spec.rb'
+- './spec/models/alert_management/metric_image_spec.rb'
+- './spec/models/analytics/cycle_analytics/aggregation_spec.rb'
+- './spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb'
+- './spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb'
+- './spec/models/analytics/cycle_analytics/project_stage_spec.rb'
+- './spec/models/analytics/cycle_analytics/project_value_stream_spec.rb'
+- './spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb'
+- './spec/models/analytics/usage_trends/measurement_spec.rb'
+- './spec/models/appearance_spec.rb'
+- './spec/models/application_record_spec.rb'
+- './spec/models/application_setting_spec.rb'
+- './spec/models/application_setting/term_spec.rb'
+- './spec/models/approval_spec.rb'
+- './spec/models/atlassian/identity_spec.rb'
+- './spec/models/audit_event_spec.rb'
+- './spec/models/authentication_event_spec.rb'
+- './spec/models/award_emoji_spec.rb'
+- './spec/models/awareness_session_spec.rb'
+- './spec/models/aws/role_spec.rb'
+- './spec/models/badges/group_badge_spec.rb'
+- './spec/models/badge_spec.rb'
+- './spec/models/badges/project_badge_spec.rb'
+- './spec/models/blob_spec.rb'
+- './spec/models/blob_viewer/base_spec.rb'
+- './spec/models/blob_viewer/changelog_spec.rb'
+- './spec/models/blob_viewer/composer_json_spec.rb'
+- './spec/models/blob_viewer/gemspec_spec.rb'
+- './spec/models/blob_viewer/gitlab_ci_yml_spec.rb'
+- './spec/models/blob_viewer/go_mod_spec.rb'
+- './spec/models/blob_viewer/license_spec.rb'
+- './spec/models/blob_viewer/markup_spec.rb'
+- './spec/models/blob_viewer/metrics_dashboard_yml_spec.rb'
+- './spec/models/blob_viewer/package_json_spec.rb'
+- './spec/models/blob_viewer/podspec_json_spec.rb'
+- './spec/models/blob_viewer/podspec_spec.rb'
+- './spec/models/blob_viewer/readme_spec.rb'
+- './spec/models/blob_viewer/route_map_spec.rb'
+- './spec/models/blob_viewer/server_side_spec.rb'
+- './spec/models/board_group_recent_visit_spec.rb'
+- './spec/models/board_project_recent_visit_spec.rb'
+- './spec/models/board_spec.rb'
+- './spec/models/broadcast_message_spec.rb'
+- './spec/models/bulk_imports/configuration_spec.rb'
+- './spec/models/bulk_imports/entity_spec.rb'
+- './spec/models/bulk_imports/export_spec.rb'
+- './spec/models/bulk_imports/export_status_spec.rb'
+- './spec/models/bulk_imports/export_upload_spec.rb'
+- './spec/models/bulk_imports/failure_spec.rb'
+- './spec/models/bulk_imports/file_transfer/group_config_spec.rb'
+- './spec/models/bulk_imports/file_transfer/project_config_spec.rb'
+- './spec/models/bulk_imports/file_transfer_spec.rb'
+- './spec/models/bulk_import_spec.rb'
+- './spec/models/bulk_imports/tracker_spec.rb'
+- './spec/models/chat_name_spec.rb'
+- './spec/models/chat_team_spec.rb'
+- './spec/models/ci/artifact_blob_spec.rb'
+- './spec/models/ci/bridge_spec.rb'
+- './spec/models/ci/build_dependencies_spec.rb'
+- './spec/models/ci/build_metadata_spec.rb'
+- './spec/models/ci/build_need_spec.rb'
+- './spec/models/ci/build_pending_state_spec.rb'
+- './spec/models/ci/build_report_result_spec.rb'
+- './spec/models/ci/build_runner_session_spec.rb'
+- './spec/models/ci/build_spec.rb'
+- './spec/models/ci/build_trace_chunks/database_spec.rb'
+- './spec/models/ci/build_trace_chunks/fog_spec.rb'
+- './spec/models/ci/build_trace_chunk_spec.rb'
+- './spec/models/ci/build_trace_chunks/redis_spec.rb'
+- './spec/models/ci/build_trace_metadata_spec.rb'
+- './spec/models/ci/build_trace_spec.rb'
+- './spec/models/ci/commit_with_pipeline_spec.rb'
+- './spec/models/ci/daily_build_group_report_result_spec.rb'
+- './spec/models/ci/deleted_object_spec.rb'
+- './spec/models/ci/freeze_period_spec.rb'
+- './spec/models/ci/freeze_period_status_spec.rb'
+- './spec/models/ci/group_spec.rb'
+- './spec/models/ci/group_variable_spec.rb'
+- './spec/models/ci/instance_variable_spec.rb'
+- './spec/models/ci/job_artifact_spec.rb'
+- './spec/models/ci/job_token/project_scope_link_spec.rb'
+- './spec/models/ci/job_token/scope_spec.rb'
+- './spec/models/ci/job_variable_spec.rb'
+- './spec/models/ci/namespace_mirror_spec.rb'
+- './spec/models/ci/pending_build_spec.rb'
+- './spec/models/ci/persistent_ref_spec.rb'
+- './spec/models/ci/pipeline_artifact_spec.rb'
+- './spec/models/ci/pipeline_config_spec.rb'
+- './spec/models/ci/pipeline_message_spec.rb'
+- './spec/models/ci/pipeline_schedule_spec.rb'
+- './spec/models/ci/pipeline_schedule_variable_spec.rb'
+- './spec/models/ci/pipeline_spec.rb'
+- './spec/models/ci/pipeline_variable_spec.rb'
+- './spec/models/ci_platform_metric_spec.rb'
+- './spec/models/ci/processable_spec.rb'
+- './spec/models/ci/project_mirror_spec.rb'
+- './spec/models/ci/ref_spec.rb'
+- './spec/models/ci/resource_group_spec.rb'
+- './spec/models/ci/resource_spec.rb'
+- './spec/models/ci/runner_namespace_spec.rb'
+- './spec/models/ci/runner_project_spec.rb'
+- './spec/models/ci/runner_spec.rb'
+- './spec/models/ci/runner_version_spec.rb'
+- './spec/models/ci/running_build_spec.rb'
+- './spec/models/ci/secure_file_spec.rb'
+- './spec/models/ci/sources/pipeline_spec.rb'
+- './spec/models/ci/stage_spec.rb'
+- './spec/models/ci/trigger_request_spec.rb'
+- './spec/models/ci/trigger_spec.rb'
+- './spec/models/ci/unit_test_failure_spec.rb'
+- './spec/models/ci/unit_test_spec.rb'
+- './spec/models/ci/variable_spec.rb'
+- './spec/models/clusters/agents/activity_event_spec.rb'
+- './spec/models/clusters/agents/group_authorization_spec.rb'
+- './spec/models/clusters/agents/implicit_authorization_spec.rb'
+- './spec/models/clusters/agent_spec.rb'
+- './spec/models/clusters/agents/project_authorization_spec.rb'
+- './spec/models/clusters/agent_token_spec.rb'
+- './spec/models/clusters/applications/cert_manager_spec.rb'
+- './spec/models/clusters/applications/cilium_spec.rb'
+- './spec/models/clusters/applications/crossplane_spec.rb'
+- './spec/models/clusters/applications/helm_spec.rb'
+- './spec/models/clusters/applications/ingress_spec.rb'
+- './spec/models/clusters/applications/jupyter_spec.rb'
+- './spec/models/clusters/applications/knative_spec.rb'
+- './spec/models/clusters/applications/prometheus_spec.rb'
+- './spec/models/clusters/applications/runner_spec.rb'
+- './spec/models/clusters/cluster_enabled_grant_spec.rb'
+- './spec/models/clusters/clusters_hierarchy_spec.rb'
+- './spec/models/clusters/cluster_spec.rb'
+- './spec/models/clusters/group_spec.rb'
+- './spec/models/clusters/integrations/prometheus_spec.rb'
+- './spec/models/clusters/kubernetes_namespace_spec.rb'
+- './spec/models/clusters/platforms/kubernetes_spec.rb'
+- './spec/models/clusters/project_spec.rb'
+- './spec/models/clusters/providers/aws_spec.rb'
+- './spec/models/clusters/providers/gcp_spec.rb'
+- './spec/models/commit_collection_spec.rb'
+- './spec/models/commit_range_spec.rb'
+- './spec/models/commit_signatures/gpg_signature_spec.rb'
+- './spec/models/commit_signatures/ssh_signature_spec.rb'
+- './spec/models/commit_signatures/x509_commit_signature_spec.rb'
+- './spec/models/commit_spec.rb'
+- './spec/models/commit_status_spec.rb'
+- './spec/models/compare_spec.rb'
+- './spec/models/concerns/access_requestable_spec.rb'
+- './spec/models/concerns/after_commit_queue_spec.rb'
+- './spec/models/concerns/approvable_base_spec.rb'
+- './spec/models/concerns/as_cte_spec.rb'
+- './spec/models/concerns/atomic_internal_id_spec.rb'
+- './spec/models/concerns/avatarable_spec.rb'
+- './spec/models/concerns/awardable_spec.rb'
+- './spec/models/concerns/awareness_spec.rb'
+- './spec/models/concerns/batch_destroy_dependent_associations_spec.rb'
+- './spec/models/concerns/batch_nullify_dependent_associations_spec.rb'
+- './spec/models/concerns/blob_language_from_git_attributes_spec.rb'
+- './spec/models/concerns/blocks_unsafe_serialization_spec.rb'
+- './spec/models/concerns/bulk_insertable_associations_spec.rb'
+- './spec/models/concerns/bulk_insert_safe_spec.rb'
+- './spec/models/concerns/cacheable_attributes_spec.rb'
+- './spec/models/concerns/cache_markdown_field_spec.rb'
+- './spec/models/concerns/cascading_namespace_setting_attribute_spec.rb'
+- './spec/models/concerns/case_sensitivity_spec.rb'
+- './spec/models/concerns/checksummable_spec.rb'
+- './spec/models/concerns/chronic_duration_attribute_spec.rb'
+- './spec/models/concerns/ci/artifactable_spec.rb'
+- './spec/models/concerns/ci/bulk_insertable_tags_spec.rb'
+- './spec/models/concerns/ci/has_deployment_name_spec.rb'
+- './spec/models/concerns/ci/has_ref_spec.rb'
+- './spec/models/concerns/ci/has_status_spec.rb'
+- './spec/models/concerns/ci/has_variable_spec.rb'
+- './spec/models/concerns/ci/maskable_spec.rb'
+- './spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb'
+- './spec/models/concerns/counter_attribute_spec.rb'
+- './spec/models/concerns/cron_schedulable_spec.rb'
+- './spec/models/concerns/cross_database_modification_spec.rb'
+- './spec/models/concerns/database_event_tracking_spec.rb'
+- './spec/models/concerns/database_reflection_spec.rb'
+- './spec/models/concerns/delete_with_limit_spec.rb'
+- './spec/models/concerns/deployment_platform_spec.rb'
+- './spec/models/concerns/deprecated_assignee_spec.rb'
+- './spec/models/concerns/discussion_on_diff_spec.rb'
+- './spec/models/concerns/each_batch_spec.rb'
+- './spec/models/concerns/editable_spec.rb'
+- './spec/models/concerns/expirable_spec.rb'
+- './spec/models/concerns/faster_cache_keys_spec.rb'
+- './spec/models/concerns/featurable_spec.rb'
+- './spec/models/concerns/feature_gate_spec.rb'
+- './spec/models/concerns/from_except_spec.rb'
+- './spec/models/concerns/from_intersect_spec.rb'
+- './spec/models/concerns/from_set_operator_spec.rb'
+- './spec/models/concerns/from_union_spec.rb'
+- './spec/models/concerns/group_descendant_spec.rb'
+- './spec/models/concerns/has_environment_scope_spec.rb'
+- './spec/models/concerns/has_user_type_spec.rb'
+- './spec/models/concerns/id_in_ordered_spec.rb'
+- './spec/models/concerns/ignorable_columns_spec.rb'
+- './spec/models/concerns/integrations/enable_ssl_verification_spec.rb'
+- './spec/models/concerns/integrations/has_data_fields_spec.rb'
+- './spec/models/concerns/integrations/reset_secret_fields_spec.rb'
+- './spec/models/concerns/issuable_link_spec.rb'
+- './spec/models/concerns/issuable_spec.rb'
+- './spec/models/concerns/legacy_bulk_insert_spec.rb'
+- './spec/models/concerns/limitable_spec.rb'
+- './spec/models/concerns/loaded_in_group_list_spec.rb'
+- './spec/models/concerns/loose_index_scan_spec.rb'
+- './spec/models/concerns/manual_inverse_association_spec.rb'
+- './spec/models/concerns/mentionable_spec.rb'
+- './spec/models/concerns/milestoneable_spec.rb'
+- './spec/models/concerns/milestoneish_spec.rb'
+- './spec/models/concerns/noteable_spec.rb'
+- './spec/models/concerns/nullify_if_blank_spec.rb'
+- './spec/models/concerns/optionally_search_spec.rb'
+- './spec/models/concerns/participable_spec.rb'
+- './spec/models/concerns/partitioned_table_spec.rb'
+- './spec/models/concerns/pg_full_text_searchable_spec.rb'
+- './spec/models/concerns/presentable_spec.rb'
+- './spec/models/concerns/project_api_compatibility_spec.rb'
+- './spec/models/concerns/project_features_compatibility_spec.rb'
+- './spec/models/concerns/prometheus_adapter_spec.rb'
+- './spec/models/concerns/protected_ref_access_spec.rb'
+- './spec/models/concerns/reactive_caching_spec.rb'
+- './spec/models/concerns/redactable_spec.rb'
+- './spec/models/concerns/redis_cacheable_spec.rb'
+- './spec/models/concerns/require_email_verification_spec.rb'
+- './spec/models/concerns/resolvable_discussion_spec.rb'
+- './spec/models/concerns/resolvable_note_spec.rb'
+- './spec/models/concerns/routable_spec.rb'
+- './spec/models/concerns/runners_token_prefixable_spec.rb'
+- './spec/models/concerns/safe_url_spec.rb'
+- './spec/models/concerns/sanitizable_spec.rb'
+- './spec/models/concerns/schedulable_spec.rb'
+- './spec/models/concerns/sensitive_serializable_hash_spec.rb'
+- './spec/models/concerns/sha_attribute_spec.rb'
+- './spec/models/concerns/sortable_spec.rb'
+- './spec/models/concerns/spammable_spec.rb'
+- './spec/models/concerns/stepable_spec.rb'
+- './spec/models/concerns/strip_attribute_spec.rb'
+- './spec/models/concerns/subscribable_spec.rb'
+- './spec/models/concerns/taggable_queries_spec.rb'
+- './spec/models/concerns/taskable_spec.rb'
+- './spec/models/concerns/token_authenticatable_spec.rb'
+- './spec/models/concerns/token_authenticatable_strategies/base_spec.rb'
+- './spec/models/concerns/token_authenticatable_strategies/digest_spec.rb'
+- './spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb'
+- './spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb'
+- './spec/models/concerns/transactions_spec.rb'
+- './spec/models/concerns/triggerable_hooks_spec.rb'
+- './spec/models/concerns/uniquify_spec.rb'
+- './spec/models/concerns/usage_statistics_spec.rb'
+- './spec/models/concerns/vulnerability_finding_helpers_spec.rb'
+- './spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb'
+- './spec/models/concerns/where_composite_spec.rb'
+- './spec/models/concerns/x509_serial_number_attribute_spec.rb'
+- './spec/models/container_expiration_policy_spec.rb'
+- './spec/models/container_registry/event_spec.rb'
+- './spec/models/container_repository_spec.rb'
+- './spec/models/context_commits_diff_spec.rb'
+- './spec/models/custom_emoji_spec.rb'
+- './spec/models/customer_relations/contact_spec.rb'
+- './spec/models/customer_relations/contact_state_counts_spec.rb'
+- './spec/models/customer_relations/issue_contact_spec.rb'
+- './spec/models/customer_relations/organization_spec.rb'
+- './spec/models/cycle_analytics/project_level_stage_adapter_spec.rb'
+- './spec/models/data_list_spec.rb'
+- './spec/models/dependency_proxy/blob_spec.rb'
+- './spec/models/dependency_proxy/group_setting_spec.rb'
+- './spec/models/dependency_proxy/image_ttl_group_policy_spec.rb'
+- './spec/models/dependency_proxy/manifest_spec.rb'
+- './spec/models/dependency_proxy/registry_spec.rb'
+- './spec/models/deploy_key_spec.rb'
+- './spec/models/deploy_keys_project_spec.rb'
+- './spec/models/deployment_cluster_spec.rb'
+- './spec/models/deployment_merge_request_spec.rb'
+- './spec/models/deployment_metrics_spec.rb'
+- './spec/models/deployment_spec.rb'
+- './spec/models/deploy_token_spec.rb'
+- './spec/models/description_version_spec.rb'
+- './spec/models/design_management/action_spec.rb'
+- './spec/models/design_management/design_action_spec.rb'
+- './spec/models/design_management/design_at_version_spec.rb'
+- './spec/models/design_management/design_collection_spec.rb'
+- './spec/models/design_management/design_spec.rb'
+- './spec/models/design_management/repository_spec.rb'
+- './spec/models/design_management/version_spec.rb'
+- './spec/models/design_user_mention_spec.rb'
+- './spec/models/dev_ops_report/metric_spec.rb'
+- './spec/models/diff_discussion_spec.rb'
+- './spec/models/diff_note_position_spec.rb'
+- './spec/models/diff_note_spec.rb'
+- './spec/models/diff_viewer/base_spec.rb'
+- './spec/models/diff_viewer/image_spec.rb'
+- './spec/models/diff_viewer/server_side_spec.rb'
+- './spec/models/discussion_note_spec.rb'
+- './spec/models/discussion_spec.rb'
+- './spec/models/draft_note_spec.rb'
+- './spec/models/email_spec.rb'
+- './spec/models/environment_spec.rb'
+- './spec/models/environment_status_spec.rb'
+- './spec/models/error_tracking/client_key_spec.rb'
+- './spec/models/error_tracking/error_event_spec.rb'
+- './spec/models/error_tracking/error_spec.rb'
+- './spec/models/error_tracking/project_error_tracking_setting_spec.rb'
+- './spec/models/event_collection_spec.rb'
+- './spec/models/event_spec.rb'
+- './spec/models/experiment_spec.rb'
+- './spec/models/experiment_subject_spec.rb'
+- './spec/models/experiment_user_spec.rb'
+- './spec/models/exported_protected_branch_spec.rb'
+- './spec/models/external_issue_spec.rb'
+- './spec/models/external_pull_request_spec.rb'
+- './spec/models/factories_spec.rb'
+- './spec/models/fork_network_member_spec.rb'
+- './spec/models/fork_network_spec.rb'
+- './spec/models/generic_commit_status_spec.rb'
+- './spec/models/gpg_key_spec.rb'
+- './spec/models/gpg_key_subkey_spec.rb'
+- './spec/models/grafana_integration_spec.rb'
+- './spec/models/group/crm_settings_spec.rb'
+- './spec/models/group_custom_attribute_spec.rb'
+- './spec/models/group_deploy_keys_group_spec.rb'
+- './spec/models/group_deploy_key_spec.rb'
+- './spec/models/group_deploy_token_spec.rb'
+- './spec/models/group_group_link_spec.rb'
+- './spec/models/group_import_state_spec.rb'
+- './spec/models/group_label_spec.rb'
+- './spec/models/groups/feature_setting_spec.rb'
+- './spec/models/group_spec.rb'
+- './spec/models/guest_spec.rb'
+- './spec/models/hooks/active_hook_filter_spec.rb'
+- './spec/models/hooks/project_hook_spec.rb'
+- './spec/models/hooks/service_hook_spec.rb'
+- './spec/models/hooks/system_hook_spec.rb'
+- './spec/models/hooks/web_hook_log_spec.rb'
+- './spec/models/hooks/web_hook_spec.rb'
+- './spec/models/identity_spec.rb'
+- './spec/models/import_export_upload_spec.rb'
+- './spec/models/import_failure_spec.rb'
+- './spec/models/incident_management/issuable_escalation_status_spec.rb'
+- './spec/models/incident_management/project_incident_management_setting_spec.rb'
+- './spec/models/incident_management/timeline_event_spec.rb'
+- './spec/models/instance_configuration_spec.rb'
+- './spec/models/instance_metadata/kas_spec.rb'
+- './spec/models/instance_metadata_spec.rb'
+- './spec/models/integrations/asana_spec.rb'
+- './spec/models/integrations/assembla_spec.rb'
+- './spec/models/integrations/bamboo_spec.rb'
+- './spec/models/integrations/base_chat_notification_spec.rb'
+- './spec/models/integrations/base_issue_tracker_spec.rb'
+- './spec/models/integrations/base_third_party_wiki_spec.rb'
+- './spec/models/integrations/bugzilla_spec.rb'
+- './spec/models/integrations/buildkite_spec.rb'
+- './spec/models/integrations/campfire_spec.rb'
+- './spec/models/integrations/chat_message/alert_message_spec.rb'
+- './spec/models/integrations/chat_message/base_message_spec.rb'
+- './spec/models/integrations/chat_message/deployment_message_spec.rb'
+- './spec/models/integrations/chat_message/issue_message_spec.rb'
+- './spec/models/integrations/chat_message/merge_message_spec.rb'
+- './spec/models/integrations/chat_message/note_message_spec.rb'
+- './spec/models/integrations/chat_message/pipeline_message_spec.rb'
+- './spec/models/integrations/chat_message/push_message_spec.rb'
+- './spec/models/integrations/chat_message/wiki_page_message_spec.rb'
+- './spec/models/integrations/confluence_spec.rb'
+- './spec/models/integrations/custom_issue_tracker_spec.rb'
+- './spec/models/integrations/datadog_spec.rb'
+- './spec/models/integrations/discord_spec.rb'
+- './spec/models/integrations/drone_ci_spec.rb'
+- './spec/models/integrations/emails_on_push_spec.rb'
+- './spec/models/integrations/every_integration_spec.rb'
+- './spec/models/integrations/ewm_spec.rb'
+- './spec/models/integrations/external_wiki_spec.rb'
+- './spec/models/integrations/field_spec.rb'
+- './spec/models/integrations/flowdock_spec.rb'
+- './spec/models/integrations/hangouts_chat_spec.rb'
+- './spec/models/integrations/harbor_spec.rb'
+- './spec/models/integrations/irker_spec.rb'
+- './spec/models/integrations/issue_tracker_data_spec.rb'
+- './spec/models/integrations/jenkins_spec.rb'
+- './spec/models/integrations/jira_spec.rb'
+- './spec/models/integrations/jira_tracker_data_spec.rb'
+- './spec/models/integrations/mattermost_slash_commands_spec.rb'
+- './spec/models/integrations/mattermost_spec.rb'
+- './spec/models/integrations/microsoft_teams_spec.rb'
+- './spec/models/integrations/mock_ci_spec.rb'
+- './spec/models/integrations/packagist_spec.rb'
+- './spec/models/integration_spec.rb'
+- './spec/models/integrations/pipelines_email_spec.rb'
+- './spec/models/integrations/pivotaltracker_spec.rb'
+- './spec/models/integrations/prometheus_spec.rb'
+- './spec/models/integrations/pumble_spec.rb'
+- './spec/models/integrations/pushover_spec.rb'
+- './spec/models/integrations/redmine_spec.rb'
+- './spec/models/integrations/shimo_spec.rb'
+- './spec/models/integrations/slack_slash_commands_spec.rb'
+- './spec/models/integrations/slack_spec.rb'
+- './spec/models/integrations/teamcity_spec.rb'
+- './spec/models/integrations/unify_circuit_spec.rb'
+- './spec/models/integrations/webex_teams_spec.rb'
+- './spec/models/integrations/youtrack_spec.rb'
+- './spec/models/integrations/zentao_spec.rb'
+- './spec/models/integrations/zentao_tracker_data_spec.rb'
+- './spec/models/internal_id_spec.rb'
+- './spec/models/issuable_severity_spec.rb'
+- './spec/models/issue_assignee_spec.rb'
+- './spec/models/issue_collection_spec.rb'
+- './spec/models/issue_email_participant_spec.rb'
+- './spec/models/issue/email_spec.rb'
+- './spec/models/issue_link_spec.rb'
+- './spec/models/issue/metrics_spec.rb'
+- './spec/models/issues/csv_import_spec.rb'
+- './spec/models/issue_spec.rb'
+- './spec/models/jira_connect_installation_spec.rb'
+- './spec/models/jira_connect_subscription_spec.rb'
+- './spec/models/jira_import_state_spec.rb'
+- './spec/models/key_spec.rb'
+- './spec/models/label_link_spec.rb'
+- './spec/models/label_note_spec.rb'
+- './spec/models/label_priority_spec.rb'
+- './spec/models/label_spec.rb'
+- './spec/models/legacy_diff_discussion_spec.rb'
+- './spec/models/legacy_diff_note_spec.rb'
+- './spec/models/lfs_download_object_spec.rb'
+- './spec/models/lfs_file_lock_spec.rb'
+- './spec/models/lfs_object_spec.rb'
+- './spec/models/lfs_objects_project_spec.rb'
+- './spec/models/license_template_spec.rb'
+- './spec/models/list_spec.rb'
+- './spec/models/list_user_preference_spec.rb'
+- './spec/models/loose_foreign_keys/deleted_record_spec.rb'
+- './spec/models/loose_foreign_keys/modification_tracker_spec.rb'
+- './spec/models/members/group_member_spec.rb'
+- './spec/models/members/last_group_owner_assigner_spec.rb'
+- './spec/models/members/member_role_spec.rb'
+- './spec/models/members/member_task_spec.rb'
+- './spec/models/member_spec.rb'
+- './spec/models/members/project_member_spec.rb'
+- './spec/models/merge_request/approval_removal_settings_spec.rb'
+- './spec/models/merge_request_assignee_spec.rb'
+- './spec/models/merge_request/cleanup_schedule_spec.rb'
+- './spec/models/merge_request_context_commit_diff_file_spec.rb'
+- './spec/models/merge_request_context_commit_spec.rb'
+- './spec/models/merge_request_diff_commit_spec.rb'
+- './spec/models/merge_request/diff_commit_user_spec.rb'
+- './spec/models/merge_request_diff_file_spec.rb'
+- './spec/models/merge_request_diff_spec.rb'
+- './spec/models/merge_request/metrics_spec.rb'
+- './spec/models/merge_request_reviewer_spec.rb'
+- './spec/models/merge_request_spec.rb'
+- './spec/models/metrics/dashboard/annotation_spec.rb'
+- './spec/models/metrics/users_starred_dashboard_spec.rb'
+- './spec/models/milestone_note_spec.rb'
+- './spec/models/milestone_release_spec.rb'
+- './spec/models/milestone_spec.rb'
+- './spec/models/ml/candidate_metric_spec.rb'
+- './spec/models/ml/candidate_param_spec.rb'
+- './spec/models/ml/candidate_spec.rb'
+- './spec/models/ml/experiment_spec.rb'
+- './spec/models/namespace/admin_note_spec.rb'
+- './spec/models/namespace/aggregation_schedule_spec.rb'
+- './spec/models/namespace_ci_cd_setting_spec.rb'
+- './spec/models/namespace/detail_spec.rb'
+- './spec/models/namespace/package_setting_spec.rb'
+- './spec/models/namespace/root_storage_statistics_spec.rb'
+- './spec/models/namespace_setting_spec.rb'
+- './spec/models/namespace_spec.rb'
+- './spec/models/namespaces/project_namespace_spec.rb'
+- './spec/models/namespace_statistics_spec.rb'
+- './spec/models/namespaces/user_namespace_spec.rb'
+- './spec/models/namespace/traversal_hierarchy_spec.rb'
+- './spec/models/network/graph_spec.rb'
+- './spec/models/note_diff_file_spec.rb'
+- './spec/models/note_spec.rb'
+- './spec/models/notification_recipient_spec.rb'
+- './spec/models/notification_setting_spec.rb'
+- './spec/models/oauth_access_grant_spec.rb'
+- './spec/models/oauth_access_token_spec.rb'
+- './spec/models/onboarding_progress_spec.rb'
+- './spec/models/operations/feature_flags_client_spec.rb'
+- './spec/models/operations/feature_flag_spec.rb'
+- './spec/models/operations/feature_flags/strategy_spec.rb'
+- './spec/models/operations/feature_flags/user_list_spec.rb'
+- './spec/models/packages/build_info_spec.rb'
+- './spec/models/packages/cleanup/policy_spec.rb'
+- './spec/models/packages/composer/cache_file_spec.rb'
+- './spec/models/packages/composer/metadatum_spec.rb'
+- './spec/models/packages/conan/file_metadatum_spec.rb'
+- './spec/models/packages/conan/metadatum_spec.rb'
+- './spec/models/packages/debian/file_entry_spec.rb'
+- './spec/models/packages/debian/file_metadatum_spec.rb'
+- './spec/models/packages/debian/group_architecture_spec.rb'
+- './spec/models/packages/debian/group_component_file_spec.rb'
+- './spec/models/packages/debian/group_component_spec.rb'
+- './spec/models/packages/debian/group_distribution_key_spec.rb'
+- './spec/models/packages/debian/group_distribution_spec.rb'
+- './spec/models/packages/debian/project_architecture_spec.rb'
+- './spec/models/packages/debian/project_component_file_spec.rb'
+- './spec/models/packages/debian/project_component_spec.rb'
+- './spec/models/packages/debian/project_distribution_key_spec.rb'
+- './spec/models/packages/debian/project_distribution_spec.rb'
+- './spec/models/packages/debian/publication_spec.rb'
+- './spec/models/packages/dependency_link_spec.rb'
+- './spec/models/packages/dependency_spec.rb'
+- './spec/models/packages/go/module_spec.rb'
+- './spec/models/packages/go/module_version_spec.rb'
+- './spec/models/packages/helm/file_metadatum_spec.rb'
+- './spec/models/packages/maven/metadatum_spec.rb'
+- './spec/models/packages/npm/metadatum_spec.rb'
+- './spec/models/packages/npm_spec.rb'
+- './spec/models/packages/nuget/dependency_link_metadatum_spec.rb'
+- './spec/models/packages/nuget/metadatum_spec.rb'
+- './spec/models/packages/package_file_build_info_spec.rb'
+- './spec/models/packages/package_file_spec.rb'
+- './spec/models/packages/package_spec.rb'
+- './spec/models/packages/pypi/metadatum_spec.rb'
+- './spec/models/packages/rubygems/metadatum_spec.rb'
+- './spec/models/packages/sem_ver_spec.rb'
+- './spec/models/packages/tag_spec.rb'
+- './spec/models/pages_deployment_spec.rb'
+- './spec/models/pages_domain_acme_order_spec.rb'
+- './spec/models/pages_domain_spec.rb'
+- './spec/models/pages/lookup_path_spec.rb'
+- './spec/models/pages/virtual_domain_spec.rb'
+- './spec/models/performance_monitoring/prometheus_dashboard_spec.rb'
+- './spec/models/performance_monitoring/prometheus_metric_spec.rb'
+- './spec/models/performance_monitoring/prometheus_panel_group_spec.rb'
+- './spec/models/performance_monitoring/prometheus_panel_spec.rb'
+- './spec/models/personal_access_token_spec.rb'
+- './spec/models/personal_snippet_spec.rb'
+- './spec/models/plan_limits_spec.rb'
+- './spec/models/plan_spec.rb'
+- './spec/models/pool_repository_spec.rb'
+- './spec/models/postgresql/detached_partition_spec.rb'
+- './spec/models/postgresql/replication_slot_spec.rb'
+- './spec/models/preloaders/commit_status_preloader_spec.rb'
+- './spec/models/preloaders/environments/deployment_preloader_spec.rb'
+- './spec/models/preloaders/group_policy_preloader_spec.rb'
+- './spec/models/preloaders/group_root_ancestor_preloader_spec.rb'
+- './spec/models/preloaders/labels_preloader_spec.rb'
+- './spec/models/preloaders/merge_request_diff_preloader_spec.rb'
+- './spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb'
+- './spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb'
+- './spec/models/preloaders/users_max_access_level_in_projects_preloader_spec.rb'
+- './spec/models/product_analytics_event_spec.rb'
+- './spec/models/programming_language_spec.rb'
+- './spec/models/project_authorization_spec.rb'
+- './spec/models/project_auto_devops_spec.rb'
+- './spec/models/project_ci_cd_setting_spec.rb'
+- './spec/models/project_custom_attribute_spec.rb'
+- './spec/models/project_daily_statistic_spec.rb'
+- './spec/models/project_deploy_token_spec.rb'
+- './spec/models/project_export_job_spec.rb'
+- './spec/models/project_feature_spec.rb'
+- './spec/models/project_feature_usage_spec.rb'
+- './spec/models/project_group_link_spec.rb'
+- './spec/models/project_import_data_spec.rb'
+- './spec/models/project_import_state_spec.rb'
+- './spec/models/project_label_spec.rb'
+- './spec/models/project_metrics_setting_spec.rb'
+- './spec/models/project_pages_metadatum_spec.rb'
+- './spec/models/project_repository_spec.rb'
+- './spec/models/projects/build_artifacts_size_refresh_spec.rb'
+- './spec/models/projects/ci_feature_usage_spec.rb'
+- './spec/models/project_setting_spec.rb'
+- './spec/models/projects/import_export/relation_export_spec.rb'
+- './spec/models/projects/import_export/relation_export_upload_spec.rb'
+- './spec/models/project_snippet_spec.rb'
+- './spec/models/project_spec.rb'
+- './spec/models/projects/project_topic_spec.rb'
+- './spec/models/projects/repository_storage_move_spec.rb'
+- './spec/models/project_statistics_spec.rb'
+- './spec/models/projects/topic_spec.rb'
+- './spec/models/projects/triggered_hooks_spec.rb'
+- './spec/models/project_team_spec.rb'
+- './spec/models/project_wiki_spec.rb'
+- './spec/models/prometheus_alert_event_spec.rb'
+- './spec/models/prometheus_alert_spec.rb'
+- './spec/models/prometheus_metric_spec.rb'
+- './spec/models/protectable_dropdown_spec.rb'
+- './spec/models/protected_branch/merge_access_level_spec.rb'
+- './spec/models/protected_branch/push_access_level_spec.rb'
+- './spec/models/protected_branch_spec.rb'
+- './spec/models/protected_tag_spec.rb'
+- './spec/models/push_event_payload_spec.rb'
+- './spec/models/push_event_spec.rb'
+- './spec/models/raw_usage_data_spec.rb'
+- './spec/models/redirect_route_spec.rb'
+- './spec/models/ref_matcher_spec.rb'
+- './spec/models/release_highlight_spec.rb'
+- './spec/models/releases/evidence_spec.rb'
+- './spec/models/releases/link_spec.rb'
+- './spec/models/release_spec.rb'
+- './spec/models/releases/source_spec.rb'
+- './spec/models/remote_mirror_spec.rb'
+- './spec/models/repository_language_spec.rb'
+- './spec/models/repository_spec.rb'
+- './spec/models/resource_label_event_spec.rb'
+- './spec/models/resource_milestone_event_spec.rb'
+- './spec/models/resource_state_event_spec.rb'
+- './spec/models/review_spec.rb'
+- './spec/models/route_spec.rb'
+- './spec/models/sent_notification_spec.rb'
+- './spec/models/sentry_issue_spec.rb'
+- './spec/models/serverless/domain_cluster_spec.rb'
+- './spec/models/serverless/domain_spec.rb'
+- './spec/models/serverless/function_spec.rb'
+- './spec/models/service_desk_setting_spec.rb'
+- './spec/models/shard_spec.rb'
+- './spec/models/snippet_blob_spec.rb'
+- './spec/models/snippet_input_action_collection_spec.rb'
+- './spec/models/snippet_input_action_spec.rb'
+- './spec/models/snippet_repository_spec.rb'
+- './spec/models/snippet_spec.rb'
+- './spec/models/snippets/repository_storage_move_spec.rb'
+- './spec/models/snippet_statistics_spec.rb'
+- './spec/models/spam_log_spec.rb'
+- './spec/models/ssh_host_key_spec.rb'
+- './spec/models/state_note_spec.rb'
+- './spec/models/subscription_spec.rb'
+- './spec/models/suggestion_spec.rb'
+- './spec/models/synthetic_note_spec.rb'
+- './spec/models/system_note_metadata_spec.rb'
+- './spec/models/term_agreement_spec.rb'
+- './spec/models/terraform/state_spec.rb'
+- './spec/models/terraform/state_version_spec.rb'
+- './spec/models/timelog_spec.rb'
+- './spec/models/time_tracking/timelog_category_spec.rb'
+- './spec/models/todo_spec.rb'
+- './spec/models/token_with_iv_spec.rb'
+- './spec/models/tree_spec.rb'
+- './spec/models/trending_project_spec.rb'
+- './spec/models/u2f_registration_spec.rb'
+- './spec/models/uploads/fog_spec.rb'
+- './spec/models/uploads/local_spec.rb'
+- './spec/models/upload_spec.rb'
+- './spec/models/user_agent_detail_spec.rb'
+- './spec/models/user_canonical_email_spec.rb'
+- './spec/models/user_custom_attribute_spec.rb'
+- './spec/models/user_detail_spec.rb'
+- './spec/models/user_highest_role_spec.rb'
+- './spec/models/user_interacted_project_spec.rb'
+- './spec/models/user_mentions/commit_user_mention_spec.rb'
+- './spec/models/user_mentions/issue_user_mention_spec.rb'
+- './spec/models/user_mentions/merge_request_user_mention_spec.rb'
+- './spec/models/user_mentions/snippet_user_mention_spec.rb'
+- './spec/models/user_preference_spec.rb'
+- './spec/models/users/banned_user_spec.rb'
+- './spec/models/users/calloutable_spec.rb'
+- './spec/models/users/callout_spec.rb'
+- './spec/models/users/credit_card_validation_spec.rb'
+- './spec/models/users/group_callout_spec.rb'
+- './spec/models/users/in_product_marketing_email_spec.rb'
+- './spec/models/users/merge_request_interaction_spec.rb'
+- './spec/models/users/namespace_callout_spec.rb'
+- './spec/models/user_spec.rb'
+- './spec/models/users/project_callout_spec.rb'
+- './spec/models/users/saved_reply_spec.rb'
+- './spec/models/users_star_project_spec.rb'
+- './spec/models/users_statistics_spec.rb'
+- './spec/models/user_status_spec.rb'
+- './spec/models/webauthn_registration_spec.rb'
+- './spec/models/web_ide_terminal_spec.rb'
+- './spec/models/wiki_directory_spec.rb'
+- './spec/models/wiki_page/meta_spec.rb'
+- './spec/models/wiki_page/slug_spec.rb'
+- './spec/models/wiki_page_spec.rb'
+- './spec/models/work_items/parent_link_spec.rb'
+- './spec/models/work_item_spec.rb'
+- './spec/models/work_items/type_spec.rb'
+- './spec/models/work_items/widgets/assignees_spec.rb'
+- './spec/models/work_items/widgets/base_spec.rb'
+- './spec/models/work_items/widgets/description_spec.rb'
+- './spec/models/work_items/widgets/hierarchy_spec.rb'
+- './spec/models/work_items/widgets/labels_spec.rb'
+- './spec/models/work_items/widgets/start_and_due_date_spec.rb'
+- './spec/models/x509_certificate_spec.rb'
+- './spec/models/x509_issuer_spec.rb'
+- './spec/models/zoom_meeting_spec.rb'
+- './spec/policies/alert_management/alert_policy_spec.rb'
+- './spec/policies/alert_management/http_integration_policy_spec.rb'
+- './spec/policies/application_setting_policy_spec.rb'
+- './spec/policies/application_setting/term_policy_spec.rb'
+- './spec/policies/award_emoji_policy_spec.rb'
+- './spec/policies/base_policy_spec.rb'
+- './spec/policies/blob_policy_spec.rb'
+- './spec/policies/board_policy_spec.rb'
+- './spec/policies/ci/bridge_policy_spec.rb'
+- './spec/policies/ci/build_policy_spec.rb'
+- './spec/policies/ci/pipeline_policy_spec.rb'
+- './spec/policies/ci/pipeline_schedule_policy_spec.rb'
+- './spec/policies/ci/trigger_policy_spec.rb'
+- './spec/policies/clusters/agent_policy_spec.rb'
+- './spec/policies/clusters/agents/activity_event_policy_spec.rb'
+- './spec/policies/clusters/agent_token_policy_spec.rb'
+- './spec/policies/clusters/cluster_policy_spec.rb'
+- './spec/policies/clusters/instance_policy_spec.rb'
+- './spec/policies/commit_policy_spec.rb'
+- './spec/policies/concerns/crud_policy_helpers_spec.rb'
+- './spec/policies/concerns/policy_actor_spec.rb'
+- './spec/policies/concerns/readonly_abilities_spec.rb'
+- './spec/policies/container_expiration_policy_policy_spec.rb'
+- './spec/policies/custom_emoji_policy_spec.rb'
+- './spec/policies/deploy_key_policy_spec.rb'
+- './spec/policies/deploy_keys_project_policy_spec.rb'
+- './spec/policies/deploy_token_policy_spec.rb'
+- './spec/policies/design_management/design_policy_spec.rb'
+- './spec/policies/environment_policy_spec.rb'
+- './spec/policies/global_policy_spec.rb'
+- './spec/policies/group_deploy_key_policy_spec.rb'
+- './spec/policies/group_deploy_keys_group_policy_spec.rb'
+- './spec/policies/group_member_policy_spec.rb'
+- './spec/policies/group_policy_spec.rb'
+- './spec/policies/identity_provider_policy_spec.rb'
+- './spec/policies/incident_management/timeline_event_policy_spec.rb'
+- './spec/policies/instance_metadata_policy_spec.rb'
+- './spec/policies/integration_policy_spec.rb'
+- './spec/policies/issuable_policy_spec.rb'
+- './spec/policies/issue_policy_spec.rb'
+- './spec/policies/merge_request_policy_spec.rb'
+- './spec/policies/metrics/dashboard/annotation_policy_spec.rb'
+- './spec/policies/namespace/root_storage_statistics_policy_spec.rb'
+- './spec/policies/namespaces/project_namespace_policy_spec.rb'
+- './spec/policies/namespaces/user_namespace_policy_spec.rb'
+- './spec/policies/note_policy_spec.rb'
+- './spec/policies/packages/package_policy_spec.rb'
+- './spec/policies/packages/policies/group_policy_spec.rb'
+- './spec/policies/packages/policies/project_policy_spec.rb'
+- './spec/policies/personal_access_token_policy_spec.rb'
+- './spec/policies/personal_snippet_policy_spec.rb'
+- './spec/policies/project_hook_policy_spec.rb'
+- './spec/policies/project_member_policy_spec.rb'
+- './spec/policies/project_policy_spec.rb'
+- './spec/policies/project_snippet_policy_spec.rb'
+- './spec/policies/project_statistics_policy_spec.rb'
+- './spec/policies/protected_branch_policy_spec.rb'
+- './spec/policies/release_policy_spec.rb'
+- './spec/policies/resource_label_event_policy_spec.rb'
+- './spec/policies/system_hook_policy_spec.rb'
+- './spec/policies/terraform/state_policy_spec.rb'
+- './spec/policies/terraform/state_version_policy_spec.rb'
+- './spec/policies/timelog_policy_spec.rb'
+- './spec/policies/todo_policy_spec.rb'
+- './spec/policies/upload_policy_spec.rb'
+- './spec/policies/user_policy_spec.rb'
+- './spec/policies/wiki_page_policy_spec.rb'
+- './spec/policies/work_item_policy_spec.rb'
+- './spec/presenters/alert_management/alert_presenter_spec.rb'
+- './spec/presenters/award_emoji_presenter_spec.rb'
+- './spec/presenters/blob_presenter_spec.rb'
+- './spec/presenters/blobs/notebook_presenter_spec.rb'
+- './spec/presenters/blobs/unfold_presenter_spec.rb'
+- './spec/presenters/ci/bridge_presenter_spec.rb'
+- './spec/presenters/ci/build_presenter_spec.rb'
+- './spec/presenters/ci/build_runner_presenter_spec.rb'
+- './spec/presenters/ci/group_variable_presenter_spec.rb'
+- './spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb'
+- './spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb'
+- './spec/presenters/ci/pipeline_presenter_spec.rb'
+- './spec/presenters/ci/stage_presenter_spec.rb'
+- './spec/presenters/ci/trigger_presenter_spec.rb'
+- './spec/presenters/ci/variable_presenter_spec.rb'
+- './spec/presenters/clusterable_presenter_spec.rb'
+- './spec/presenters/clusters/cluster_presenter_spec.rb'
+- './spec/presenters/commit_presenter_spec.rb'
+- './spec/presenters/commit_status_presenter_spec.rb'
+- './spec/presenters/dev_ops_report/metric_presenter_spec.rb'
+- './spec/presenters/event_presenter_spec.rb'
+- './spec/presenters/gitlab/blame_presenter_spec.rb'
+- './spec/presenters/group_clusterable_presenter_spec.rb'
+- './spec/presenters/group_member_presenter_spec.rb'
+- './spec/presenters/instance_clusterable_presenter_spec.rb'
+- './spec/presenters/issue_presenter_spec.rb'
+- './spec/presenters/label_presenter_spec.rb'
+- './spec/presenters/merge_request_presenter_spec.rb'
+- './spec/presenters/milestone_presenter_spec.rb'
+- './spec/presenters/packages/composer/packages_presenter_spec.rb'
+- './spec/presenters/packages/conan/package_presenter_spec.rb'
+- './spec/presenters/packages/detail/package_presenter_spec.rb'
+- './spec/presenters/packages/helm/index_presenter_spec.rb'
+- './spec/presenters/packages/npm/package_presenter_spec.rb'
+- './spec/presenters/packages/nuget/package_metadata_presenter_spec.rb'
+- './spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb'
+- './spec/presenters/packages/nuget/packages_versions_presenter_spec.rb'
+- './spec/presenters/packages/nuget/search_results_presenter_spec.rb'
+- './spec/presenters/packages/nuget/service_index_presenter_spec.rb'
+- './spec/presenters/packages/pypi/simple_index_presenter_spec.rb'
+- './spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb'
+- './spec/presenters/pages_domain_presenter_spec.rb'
+- './spec/presenters/project_clusterable_presenter_spec.rb'
+- './spec/presenters/project_hook_presenter_spec.rb'
+- './spec/presenters/project_member_presenter_spec.rb'
+- './spec/presenters/project_presenter_spec.rb'
+- './spec/presenters/projects/import_export/project_export_presenter_spec.rb'
+- './spec/presenters/projects/security/configuration_presenter_spec.rb'
+- './spec/presenters/projects/settings/deploy_keys_presenter_spec.rb'
+- './spec/presenters/prometheus_alert_presenter_spec.rb'
+- './spec/presenters/release_presenter_spec.rb'
+- './spec/presenters/releases/link_presenter_spec.rb'
+- './spec/presenters/search_service_presenter_spec.rb'
+- './spec/presenters/sentry_error_presenter_spec.rb'
+- './spec/presenters/service_hook_presenter_spec.rb'
+- './spec/presenters/snippet_blob_presenter_spec.rb'
+- './spec/presenters/snippet_presenter_spec.rb'
+- './spec/presenters/terraform/modules_presenter_spec.rb'
+- './spec/presenters/tree_entry_presenter_spec.rb'
+- './spec/presenters/user_presenter_spec.rb'
+- './spec/presenters/web_hook_log_presenter_spec.rb'
+- './spec/rack_servers/puma_spec.rb'
+- './spec/requests/abuse_reports_controller_spec.rb'
+- './spec/requests/admin/applications_controller_spec.rb'
+- './spec/requests/admin/background_migrations_controller_spec.rb'
+- './spec/requests/admin/batched_jobs_controller_spec.rb'
+- './spec/requests/admin/broadcast_messages_controller_spec.rb'
+- './spec/requests/admin/clusters/integrations_controller_spec.rb'
+- './spec/requests/admin/impersonation_tokens_controller_spec.rb'
+- './spec/requests/admin/integrations_controller_spec.rb'
+- './spec/requests/admin/version_check_controller_spec.rb'
+- './spec/requests/api/access_requests_spec.rb'
+- './spec/requests/api/admin/batched_background_migrations_spec.rb'
+- './spec/requests/api/admin/ci/variables_spec.rb'
+- './spec/requests/api/admin/instance_clusters_spec.rb'
+- './spec/requests/api/admin/plan_limits_spec.rb'
+- './spec/requests/api/admin/sidekiq_spec.rb'
+- './spec/requests/api/alert_management_alerts_spec.rb'
+- './spec/requests/api/api_guard/admin_mode_middleware_spec.rb'
+- './spec/requests/api/api_guard/response_coercer_middleware_spec.rb'
+- './spec/requests/api/api_spec.rb'
+- './spec/requests/api/appearance_spec.rb'
+- './spec/requests/api/applications_spec.rb'
+- './spec/requests/api/avatar_spec.rb'
+- './spec/requests/api/award_emoji_spec.rb'
+- './spec/requests/api/badges_spec.rb'
+- './spec/requests/api/boards_spec.rb'
+- './spec/requests/api/branches_spec.rb'
+- './spec/requests/api/broadcast_messages_spec.rb'
+- './spec/requests/api/bulk_imports_spec.rb'
+- './spec/requests/api/ci/job_artifacts_spec.rb'
+- './spec/requests/api/ci/jobs_spec.rb'
+- './spec/requests/api/ci/pipeline_schedules_spec.rb'
+- './spec/requests/api/ci/pipelines_spec.rb'
+- './spec/requests/api/ci/resource_groups_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_reset_spec.rb'
+- './spec/requests/api/ci/runner/runners_verify_post_spec.rb'
+- './spec/requests/api/ci/runners_reset_registration_token_spec.rb'
+- './spec/requests/api/ci/runners_spec.rb'
+- './spec/requests/api/ci/secure_files_spec.rb'
+- './spec/requests/api/ci/triggers_spec.rb'
+- './spec/requests/api/ci/variables_spec.rb'
+- './spec/requests/api/clusters/agents_spec.rb'
+- './spec/requests/api/clusters/agent_tokens_spec.rb'
+- './spec/requests/api/commits_spec.rb'
+- './spec/requests/api/commit_statuses_spec.rb'
+- './spec/requests/api/composer_packages_spec.rb'
+- './spec/requests/api/conan_instance_packages_spec.rb'
+- './spec/requests/api/conan_project_packages_spec.rb'
+- './spec/requests/api/container_registry_event_spec.rb'
+- './spec/requests/api/container_repositories_spec.rb'
+- './spec/requests/api/debian_group_packages_spec.rb'
+- './spec/requests/api/debian_project_packages_spec.rb'
+- './spec/requests/api/dependency_proxy_spec.rb'
+- './spec/requests/api/deploy_keys_spec.rb'
+- './spec/requests/api/deployments_spec.rb'
+- './spec/requests/api/deploy_tokens_spec.rb'
+- './spec/requests/api/discussions_spec.rb'
+- './spec/requests/api/doorkeeper_access_spec.rb'
+- './spec/requests/api/environments_spec.rb'
+- './spec/requests/api/error_tracking/client_keys_spec.rb'
+- './spec/requests/api/error_tracking/collector_spec.rb'
+- './spec/requests/api/error_tracking/project_settings_spec.rb'
+- './spec/requests/api/events_spec.rb'
+- './spec/requests/api/feature_flags_spec.rb'
+- './spec/requests/api/feature_flags_user_lists_spec.rb'
+- './spec/requests/api/features_spec.rb'
+- './spec/requests/api/files_spec.rb'
+- './spec/requests/api/freeze_periods_spec.rb'
+- './spec/requests/api/generic_packages_spec.rb'
+- './spec/requests/api/geo_spec.rb'
+- './spec/requests/api/go_proxy_spec.rb'
+- './spec/requests/api/graphql/boards/board_list_issues_query_spec.rb'
+- './spec/requests/api/graphql/boards/board_list_query_spec.rb'
+- './spec/requests/api/graphql/boards/board_lists_query_spec.rb'
+- './spec/requests/api/graphql/boards/boards_query_spec.rb'
+- './spec/requests/api/graphql/ci/application_setting_spec.rb'
+- './spec/requests/api/graphql/ci/ci_cd_setting_spec.rb'
+- './spec/requests/api/graphql/ci/config_spec.rb'
+- './spec/requests/api/graphql/ci/groups_spec.rb'
+- './spec/requests/api/graphql/ci/group_variables_spec.rb'
+- './spec/requests/api/graphql/ci/instance_variables_spec.rb'
+- './spec/requests/api/graphql/ci/job_artifacts_spec.rb'
+- './spec/requests/api/graphql/ci/job_spec.rb'
+- './spec/requests/api/graphql/ci/jobs_spec.rb'
+- './spec/requests/api/graphql/ci/manual_variables_spec.rb'
+- './spec/requests/api/graphql/ci/pipelines_spec.rb'
+- './spec/requests/api/graphql/ci/project_variables_spec.rb'
+- './spec/requests/api/graphql/ci/runner_spec.rb'
+- './spec/requests/api/graphql/ci/runners_spec.rb'
+- './spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb'
+- './spec/requests/api/graphql/ci/stages_spec.rb'
+- './spec/requests/api/graphql/ci/template_spec.rb'
+- './spec/requests/api/graphql/container_repository/container_repository_details_spec.rb'
+- './spec/requests/api/graphql/crm/contacts_spec.rb'
+- './spec/requests/api/graphql/current_user/groups_query_spec.rb'
+- './spec/requests/api/graphql/current_user_query_spec.rb'
+- './spec/requests/api/graphql/current_user/todos_query_spec.rb'
+- './spec/requests/api/graphql/current_user_todos_spec.rb'
+- './spec/requests/api/graphql/custom_emoji_query_spec.rb'
+- './spec/requests/api/graphql/gitlab_schema_spec.rb'
+- './spec/requests/api/graphql/group/container_repositories_spec.rb'
+- './spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb'
+- './spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb'
+- './spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb'
+- './spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb'
+- './spec/requests/api/graphql/group/group_members_spec.rb'
+- './spec/requests/api/graphql/group/issues_spec.rb'
+- './spec/requests/api/graphql/group/labels_query_spec.rb'
+- './spec/requests/api/graphql/group/merge_requests_spec.rb'
+- './spec/requests/api/graphql/group/milestones_spec.rb'
+- './spec/requests/api/graphql/group/packages_spec.rb'
+- './spec/requests/api/graphql/group_query_spec.rb'
+- './spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb'
+- './spec/requests/api/graphql/group/timelogs_spec.rb'
+- './spec/requests/api/graphql/group/work_item_types_spec.rb'
+- './spec/requests/api/graphql/issue/issue_spec.rb'
+- './spec/requests/api/graphql/issue_status_counts_spec.rb'
+- './spec/requests/api/graphql/merge_request/merge_request_spec.rb'
+- './spec/requests/api/graphql/metadata_query_spec.rb'
+- './spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
+- './spec/requests/api/graphql/metrics/dashboard_query_spec.rb'
+- './spec/requests/api/graphql/milestone_spec.rb'
+- './spec/requests/api/graphql/multiplexed_queries_spec.rb'
+- './spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb'
+- './spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb'
+- './spec/requests/api/graphql/mutations/award_emojis/add_spec.rb'
+- './spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb'
+- './spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb'
+- './spec/requests/api/graphql/mutations/boards/create_spec.rb'
+- './spec/requests/api/graphql/mutations/boards/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb'
+- './spec/requests/api/graphql/mutations/boards/lists/create_spec.rb'
+- './spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/boards/lists/update_spec.rb'
+- './spec/requests/api/graphql/mutations/branches/create_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/job_play_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/job_retry_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb'
+- './spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb'
+- './spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb'
+- './spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb'
+- './spec/requests/api/graphql/mutations/commits/create_spec.rb'
+- './spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb'
+- './spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb'
+- './spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb'
+- './spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb'
+- './spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb'
+- './spec/requests/api/graphql/mutations/design_management/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/design_management/move_spec.rb'
+- './spec/requests/api/graphql/mutations/design_management/upload_spec.rb'
+- './spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb'
+- './spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb'
+- './spec/requests/api/graphql/mutations/groups/update_spec.rb'
+- './spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb'
+- './spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb'
+- './spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/create_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/move_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_locked_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_severity_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb'
+- './spec/requests/api/graphql/mutations/issues/update_spec.rb'
+- './spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb'
+- './spec/requests/api/graphql/mutations/jira_import/start_spec.rb'
+- './spec/requests/api/graphql/mutations/labels/create_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/accept_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/create_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb'
+- './spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb'
+- './spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb'
+- './spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/create/note_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb'
+- './spec/requests/api/graphql/mutations/notes/update/note_spec.rb'
+- './spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb'
+- './spec/requests/api/graphql/mutations/packages/destroy_file_spec.rb'
+- './spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb'
+- './spec/requests/api/graphql/mutations/packages/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb'
+- './spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb'
+- './spec/requests/api/graphql/mutations/releases/create_spec.rb'
+- './spec/requests/api/graphql/mutations/releases/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/releases/update_spec.rb'
+- './spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb'
+- './spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb'
+- './spec/requests/api/graphql/mutations/snippets/create_spec.rb'
+- './spec/requests/api/graphql/mutations/snippets/destroy_spec.rb'
+- './spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb'
+- './spec/requests/api/graphql/mutations/snippets/update_spec.rb'
+- './spec/requests/api/graphql/mutations/timelogs/create_spec.rb'
+- './spec/requests/api/graphql/mutations/timelogs/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/todos/create_spec.rb'
+- './spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb'
+- './spec/requests/api/graphql/mutations/todos/mark_done_spec.rb'
+- './spec/requests/api/graphql/mutations/todos/restore_many_spec.rb'
+- './spec/requests/api/graphql/mutations/todos/restore_spec.rb'
+- './spec/requests/api/graphql/mutations/uploads/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/user_callouts/create_spec.rb'
+- './spec/requests/api/graphql/mutations/user_preferences/update_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/create_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/delete_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/update_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/update_task_spec.rb'
+- './spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb'
+- './spec/requests/api/graphql/namespace/package_settings_spec.rb'
+- './spec/requests/api/graphql/namespace/projects_spec.rb'
+- './spec/requests/api/graphql/namespace_query_spec.rb'
+- './spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb'
+- './spec/requests/api/graphql/packages/composer_spec.rb'
+- './spec/requests/api/graphql/packages/conan_spec.rb'
+- './spec/requests/api/graphql/packages/helm_spec.rb'
+- './spec/requests/api/graphql/packages/maven_spec.rb'
+- './spec/requests/api/graphql/packages/nuget_spec.rb'
+- './spec/requests/api/graphql/packages/package_spec.rb'
+- './spec/requests/api/graphql/packages/pypi_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alerts_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb'
+- './spec/requests/api/graphql/project/alert_management/integrations_spec.rb'
+- './spec/requests/api/graphql/project/base_service_spec.rb'
+- './spec/requests/api/graphql/project/cluster_agents_spec.rb'
+- './spec/requests/api/graphql/project/container_expiration_policy_spec.rb'
+- './spec/requests/api/graphql/project/container_repositories_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/project/fork_targets_spec.rb'
+- './spec/requests/api/graphql/project/grafana_integration_spec.rb'
+- './spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb'
+- './spec/requests/api/graphql/project/issue/design_collection/version_spec.rb'
+- './spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb'
+- './spec/requests/api/graphql/project/issue/designs/designs_spec.rb'
+- './spec/requests/api/graphql/project/issue/designs/notes_spec.rb'
+- './spec/requests/api/graphql/project/issue/notes_spec.rb'
+- './spec/requests/api/graphql/project/issue_spec.rb'
+- './spec/requests/api/graphql/project/issues_spec.rb'
+- './spec/requests/api/graphql/project/jira_import_spec.rb'
+- './spec/requests/api/graphql/project/jira_projects_spec.rb'
+- './spec/requests/api/graphql/project/jira_service_spec.rb'
+- './spec/requests/api/graphql/project/jobs_spec.rb'
+- './spec/requests/api/graphql/project/labels_query_spec.rb'
+- './spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb'
+- './spec/requests/api/graphql/project/merge_request/pipelines_spec.rb'
+- './spec/requests/api/graphql/project/merge_request_spec.rb'
+- './spec/requests/api/graphql/project/merge_requests_spec.rb'
+- './spec/requests/api/graphql/project/milestones_spec.rb'
+- './spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb'
+- './spec/requests/api/graphql/project/packages_spec.rb'
+- './spec/requests/api/graphql/project/pipeline_spec.rb'
+- './spec/requests/api/graphql/project/project_members_spec.rb'
+- './spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb'
+- './spec/requests/api/graphql/project/project_statistics_spec.rb'
+- './spec/requests/api/graphql/project_query_spec.rb'
+- './spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb'
+- './spec/requests/api/graphql/project/release_spec.rb'
+- './spec/requests/api/graphql/project/releases_spec.rb'
+- './spec/requests/api/graphql/project/repository/blobs_spec.rb'
+- './spec/requests/api/graphql/project/repository_spec.rb'
+- './spec/requests/api/graphql/project/terraform/state_spec.rb'
+- './spec/requests/api/graphql/project/terraform/states_spec.rb'
+- './spec/requests/api/graphql/project/tree/tree_spec.rb'
+- './spec/requests/api/graphql/project/work_items_spec.rb'
+- './spec/requests/api/graphql/project/work_item_types_spec.rb'
+- './spec/requests/api/graphql/query_spec.rb'
+- './spec/requests/api/graphql/read_only_spec.rb'
+- './spec/requests/api/graphql/snippets_spec.rb'
+- './spec/requests/api/graphql_spec.rb'
+- './spec/requests/api/graphql/tasks/task_completion_status_spec.rb'
+- './spec/requests/api/graphql/terraform/state/delete_spec.rb'
+- './spec/requests/api/graphql/terraform/state/lock_spec.rb'
+- './spec/requests/api/graphql/terraform/state/unlock_spec.rb'
+- './spec/requests/api/graphql/todo_query_spec.rb'
+- './spec/requests/api/graphql/usage_trends_measurements_spec.rb'
+- './spec/requests/api/graphql/user/group_member_query_spec.rb'
+- './spec/requests/api/graphql/user/project_member_query_spec.rb'
+- './spec/requests/api/graphql/user_query_spec.rb'
+- './spec/requests/api/graphql/user_spec.rb'
+- './spec/requests/api/graphql/users_spec.rb'
+- './spec/requests/api/graphql/user/starred_projects_query_spec.rb'
+- './spec/requests/api/graphql/work_item_spec.rb'
+- './spec/requests/api/group_avatar_spec.rb'
+- './spec/requests/api/group_boards_spec.rb'
+- './spec/requests/api/group_clusters_spec.rb'
+- './spec/requests/api/group_container_repositories_spec.rb'
+- './spec/requests/api/group_debian_distributions_spec.rb'
+- './spec/requests/api/group_export_spec.rb'
+- './spec/requests/api/group_import_spec.rb'
+- './spec/requests/api/group_labels_spec.rb'
+- './spec/requests/api/group_milestones_spec.rb'
+- './spec/requests/api/group_packages_spec.rb'
+- './spec/requests/api/groups_spec.rb'
+- './spec/requests/api/group_variables_spec.rb'
+- './spec/requests/api/helm_packages_spec.rb'
+- './spec/requests/api/helpers_spec.rb'
+- './spec/requests/api/import_bitbucket_server_spec.rb'
+- './spec/requests/api/import_github_spec.rb'
+- './spec/requests/api/integrations/jira_connect/subscriptions_spec.rb'
+- './spec/requests/api/integrations/slack/events_spec.rb'
+- './spec/requests/api/integrations_spec.rb'
+- './spec/requests/api/internal/base_spec.rb'
+- './spec/requests/api/internal/container_registry/migration_spec.rb'
+- './spec/requests/api/internal/error_tracking_spec.rb'
+- './spec/requests/api/internal/kubernetes_spec.rb'
+- './spec/requests/api/internal/lfs_spec.rb'
+- './spec/requests/api/internal/mail_room_spec.rb'
+- './spec/requests/api/internal/pages_spec.rb'
+- './spec/requests/api/internal/workhorse_spec.rb'
+- './spec/requests/api/invitations_spec.rb'
+- './spec/requests/api/issue_links_spec.rb'
+- './spec/requests/api/issues/get_group_issues_spec.rb'
+- './spec/requests/api/issues/get_project_issues_spec.rb'
+- './spec/requests/api/issues/issues_spec.rb'
+- './spec/requests/api/issues/post_projects_issues_spec.rb'
+- './spec/requests/api/issues/put_projects_issues_spec.rb'
+- './spec/requests/api/keys_spec.rb'
+- './spec/requests/api/labels_spec.rb'
+- './spec/requests/api/lint_spec.rb'
+- './spec/requests/api/markdown_golden_master_spec.rb'
+- './spec/requests/api/markdown_snapshot_spec.rb'
+- './spec/requests/api/markdown_spec.rb'
+- './spec/requests/api/maven_packages_spec.rb'
+- './spec/requests/api/members_spec.rb'
+- './spec/requests/api/merge_request_approvals_spec.rb'
+- './spec/requests/api/merge_request_diffs_spec.rb'
+- './spec/requests/api/merge_requests_spec.rb'
+- './spec/requests/api/metadata_spec.rb'
+- './spec/requests/api/metrics/dashboard/annotations_spec.rb'
+- './spec/requests/api/metrics/user_starred_dashboards_spec.rb'
+- './spec/requests/api/namespaces_spec.rb'
+- './spec/requests/api/notes_spec.rb'
+- './spec/requests/api/notification_settings_spec.rb'
+- './spec/requests/api/npm_instance_packages_spec.rb'
+- './spec/requests/api/npm_project_packages_spec.rb'
+- './spec/requests/api/nuget_group_packages_spec.rb'
+- './spec/requests/api/nuget_project_packages_spec.rb'
+- './spec/requests/api/oauth_tokens_spec.rb'
+- './spec/requests/api/package_files_spec.rb'
+- './spec/requests/api/pages_domains_spec.rb'
+- './spec/requests/api/pages/internal_access_spec.rb'
+- './spec/requests/api/pages/pages_spec.rb'
+- './spec/requests/api/pages/private_access_spec.rb'
+- './spec/requests/api/pages/public_access_spec.rb'
+- './spec/requests/api/performance_bar_spec.rb'
+- './spec/requests/api/personal_access_tokens_spec.rb'
+- './spec/requests/api/project_clusters_spec.rb'
+- './spec/requests/api/project_container_repositories_spec.rb'
+- './spec/requests/api/project_debian_distributions_spec.rb'
+- './spec/requests/api/project_events_spec.rb'
+- './spec/requests/api/project_export_spec.rb'
+- './spec/requests/api/project_hooks_spec.rb'
+- './spec/requests/api/project_import_spec.rb'
+- './spec/requests/api/project_milestones_spec.rb'
+- './spec/requests/api/project_packages_spec.rb'
+- './spec/requests/api/project_repository_storage_moves_spec.rb'
+- './spec/requests/api/project_snapshots_spec.rb'
+- './spec/requests/api/project_snippets_spec.rb'
+- './spec/requests/api/projects_spec.rb'
+- './spec/requests/api/project_statistics_spec.rb'
+- './spec/requests/api/project_templates_spec.rb'
+- './spec/requests/api/protected_branches_spec.rb'
+- './spec/requests/api/protected_tags_spec.rb'
+- './spec/requests/api/pypi_packages_spec.rb'
+- './spec/requests/api/release/links_spec.rb'
+- './spec/requests/api/releases_spec.rb'
+- './spec/requests/api/remote_mirrors_spec.rb'
+- './spec/requests/api/repositories_spec.rb'
+- './spec/requests/api/resource_access_tokens_spec.rb'
+- './spec/requests/api/resource_label_events_spec.rb'
+- './spec/requests/api/resource_milestone_events_spec.rb'
+- './spec/requests/api/resource_state_events_spec.rb'
+- './spec/requests/api/rubygem_packages_spec.rb'
+- './spec/requests/api/search_spec.rb'
+- './spec/requests/api/settings_spec.rb'
+- './spec/requests/api/sidekiq_metrics_spec.rb'
+- './spec/requests/api/snippet_repository_storage_moves_spec.rb'
+- './spec/requests/api/snippets_spec.rb'
+- './spec/requests/api/statistics_spec.rb'
+- './spec/requests/api/submodules_spec.rb'
+- './spec/requests/api/suggestions_spec.rb'
+- './spec/requests/api/system_hooks_spec.rb'
+- './spec/requests/api/tags_spec.rb'
+- './spec/requests/api/task_completion_status_spec.rb'
+- './spec/requests/api/templates_spec.rb'
+- './spec/requests/api/terraform/modules/v1/packages_spec.rb'
+- './spec/requests/api/terraform/state_spec.rb'
+- './spec/requests/api/terraform/state_version_spec.rb'
+- './spec/requests/api/todos_spec.rb'
+- './spec/requests/api/topics_spec.rb'
+- './spec/requests/api/unleash_spec.rb'
+- './spec/requests/api/usage_data_non_sql_metrics_spec.rb'
+- './spec/requests/api/usage_data_queries_spec.rb'
+- './spec/requests/api/usage_data_spec.rb'
+- './spec/requests/api/user_counts_spec.rb'
+- './spec/requests/api/users_preferences_spec.rb'
+- './spec/requests/api/users_spec.rb'
+- './spec/requests/api/v3/github_spec.rb'
+- './spec/requests/api/version_spec.rb'
+- './spec/requests/api/wikis_spec.rb'
+- './spec/requests/boards/lists_controller_spec.rb'
+- './spec/requests/concerns/planning_hierarchy_spec.rb'
+- './spec/requests/content_security_policy_spec.rb'
+- './spec/requests/dashboard_controller_spec.rb'
+- './spec/requests/dashboard/projects_controller_spec.rb'
+- './spec/requests/git_http_spec.rb'
+- './spec/requests/groups/autocomplete_sources_spec.rb'
+- './spec/requests/groups/clusters/integrations_controller_spec.rb'
+- './spec/requests/groups_controller_spec.rb'
+- './spec/requests/groups/crm/contacts_controller_spec.rb'
+- './spec/requests/groups/crm/organizations_controller_spec.rb'
+- './spec/requests/groups/deploy_tokens_controller_spec.rb'
+- './spec/requests/groups/email_campaigns_controller_spec.rb'
+- './spec/requests/groups/harbor/artifacts_controller_spec.rb'
+- './spec/requests/groups/harbor/repositories_controller_spec.rb'
+- './spec/requests/groups/harbor/tags_controller_spec.rb'
+- './spec/requests/groups/milestones_controller_spec.rb'
+- './spec/requests/groups/registry/repositories_controller_spec.rb'
+- './spec/requests/groups/settings/access_tokens_controller_spec.rb'
+- './spec/requests/groups/settings/applications_controller_spec.rb'
+- './spec/requests/health_controller_spec.rb'
+- './spec/requests/ide_controller_spec.rb'
+- './spec/requests/import/gitlab_groups_controller_spec.rb'
+- './spec/requests/import/gitlab_projects_controller_spec.rb'
+- './spec/requests/import/url_controller_spec.rb'
+- './spec/requests/jira_authorizations_spec.rb'
+- './spec/requests/jira_connect/installations_controller_spec.rb'
+- './spec/requests/jira_connect/oauth_application_ids_controller_spec.rb'
+- './spec/requests/jira_connect/oauth_callbacks_controller_spec.rb'
+- './spec/requests/jira_connect/subscriptions_controller_spec.rb'
+- './spec/requests/jira_connect/users_controller_spec.rb'
+- './spec/requests/jira_routing_spec.rb'
+- './spec/requests/jwks_controller_spec.rb'
+- './spec/requests/jwt_controller_spec.rb'
+- './spec/requests/lfs_http_spec.rb'
+- './spec/requests/lfs_locks_api_spec.rb'
+- './spec/requests/mailgun/webhooks_controller_spec.rb'
+- './spec/requests/oauth/applications_controller_spec.rb'
+- './spec/requests/oauth/authorizations_controller_spec.rb'
+- './spec/requests/oauth/tokens_controller_spec.rb'
+- './spec/requests/oauth_tokens_spec.rb'
+- './spec/requests/openid_connect_spec.rb'
+- './spec/requests/product_analytics/collector_app_attack_spec.rb'
+- './spec/requests/product_analytics/collector_app_spec.rb'
+- './spec/requests/profiles/notifications_controller_spec.rb'
+- './spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb'
+- './spec/requests/projects/cluster_agents_controller_spec.rb'
+- './spec/requests/projects/clusters/integrations_controller_spec.rb'
+- './spec/requests/projects/commits_controller_spec.rb'
+- './spec/requests/projects_controller_spec.rb'
+- './spec/requests/projects/cycle_analytics_events_spec.rb'
+- './spec/requests/projects/environments_controller_spec.rb'
+- './spec/requests/projects/google_cloud/configuration_controller_spec.rb'
+- './spec/requests/projects/google_cloud/databases_controller_spec.rb'
+- './spec/requests/projects/google_cloud/deployments_controller_spec.rb'
+- './spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb'
+- './spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb'
+- './spec/requests/projects/google_cloud/service_accounts_controller_spec.rb'
+- './spec/requests/projects/harbor/artifacts_controller_spec.rb'
+- './spec/requests/projects/harbor/repositories_controller_spec.rb'
+- './spec/requests/projects/harbor/tags_controller_spec.rb'
+- './spec/requests/projects/incident_management/pagerduty_incidents_spec.rb'
+- './spec/requests/projects/integrations/shimos_controller_spec.rb'
+- './spec/requests/projects/issue_links_controller_spec.rb'
+- './spec/requests/projects/issues_controller_spec.rb'
+- './spec/requests/projects/issues/discussions_spec.rb'
+- './spec/requests/projects/merge_requests/content_spec.rb'
+- './spec/requests/projects/merge_requests/context_commit_diffs_spec.rb'
+- './spec/requests/projects/merge_requests_controller_spec.rb'
+- './spec/requests/projects/merge_requests/creations_spec.rb'
+- './spec/requests/projects/merge_requests/diffs_spec.rb'
+- './spec/requests/projects/merge_requests_discussions_spec.rb'
+- './spec/requests/projects/merge_requests_spec.rb'
+- './spec/requests/projects/metrics/dashboards/builder_spec.rb'
+- './spec/requests/projects/metrics_dashboard_spec.rb'
+- './spec/requests/projects/noteable_notes_spec.rb'
+- './spec/requests/projects/pipelines_controller_spec.rb'
+- './spec/requests/projects/redirect_controller_spec.rb'
+- './spec/requests/projects/releases_controller_spec.rb'
+- './spec/requests/projects/settings/access_tokens_controller_spec.rb'
+- './spec/requests/projects/settings/packages_and_registries_controller_spec.rb'
+- './spec/requests/projects/tags_controller_spec.rb'
+- './spec/requests/projects/uploads_spec.rb'
+- './spec/requests/projects/usage_quotas_spec.rb'
+- './spec/requests/projects/work_items_spec.rb'
+- './spec/requests/pwa_controller_spec.rb'
+- './spec/requests/rack_attack_global_spec.rb'
+- './spec/requests/recursive_webhook_detection_spec.rb'
+- './spec/requests/robots_txt_spec.rb'
+- './spec/requests/runner_setup_controller_spec.rb'
+- './spec/requests/sandbox_controller_spec.rb'
+- './spec/requests/search_controller_spec.rb'
+- './spec/requests/self_monitoring_project_spec.rb'
+- './spec/requests/sessions_spec.rb'
+- './spec/requests/terraform/services_controller_spec.rb'
+- './spec/requests/user_activity_spec.rb'
+- './spec/requests/user_avatar_spec.rb'
+- './spec/requests/users_controller_spec.rb'
+- './spec/requests/user_sends_malformed_strings_spec.rb'
+- './spec/requests/users/group_callouts_spec.rb'
+- './spec/requests/users/namespace_callouts_spec.rb'
+- './spec/requests/user_spoofs_ip_spec.rb'
+- './spec/requests/users/project_callouts_spec.rb'
+- './spec/requests/verifies_with_email_spec.rb'
+- './spec/requests/whats_new_controller_spec.rb'
+- './spec/routing/admin_routing_spec.rb'
+- './spec/routing/environments_spec.rb'
+- './spec/routing/git_http_routing_spec.rb'
+- './spec/routing/group_routing_spec.rb'
+- './spec/routing/import_routing_spec.rb'
+- './spec/routing/notifications_routing_spec.rb'
+- './spec/routing/openid_connect_spec.rb'
+- './spec/routing/project_routing_spec.rb'
+- './spec/routing/projects/security/configuration_controller_routing_spec.rb'
+- './spec/routing/routing_spec.rb'
+- './spec/routing/uploads_routing_spec.rb'
+- './spec/scripts/changed-feature-flags_spec.rb'
+- './spec/scripts/determine-qa-tests_spec.rb'
+- './spec/scripts/failed_tests_spec.rb'
+- './spec/scripts/lib/glfm/parse_examples_spec.rb'
+- './spec/scripts/lib/glfm/shared_spec.rb'
+- './spec/scripts/lib/glfm/update_example_snapshots_spec.rb'
+- './spec/scripts/lib/glfm/update_specification_spec.rb'
+- './spec/scripts/pipeline_test_report_builder_spec.rb'
+- './spec/scripts/setup/find_jh_branch_spec.rb'
+- './spec/scripts/trigger-build_spec.rb'
+- './spec/serializers/accessibility_error_entity_spec.rb'
+- './spec/serializers/accessibility_reports_comparer_entity_spec.rb'
+- './spec/serializers/accessibility_reports_comparer_serializer_spec.rb'
+- './spec/serializers/admin/user_entity_spec.rb'
+- './spec/serializers/admin/user_serializer_spec.rb'
+- './spec/serializers/analytics_build_entity_spec.rb'
+- './spec/serializers/analytics_build_serializer_spec.rb'
+- './spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb'
+- './spec/serializers/analytics_issue_entity_spec.rb'
+- './spec/serializers/analytics_issue_serializer_spec.rb'
+- './spec/serializers/analytics_merge_request_serializer_spec.rb'
+- './spec/serializers/analytics_summary_serializer_spec.rb'
+- './spec/serializers/base_discussion_entity_spec.rb'
+- './spec/serializers/blob_entity_spec.rb'
+- './spec/serializers/board_serializer_spec.rb'
+- './spec/serializers/board_simple_entity_spec.rb'
+- './spec/serializers/build_action_entity_spec.rb'
+- './spec/serializers/build_artifact_entity_spec.rb'
+- './spec/serializers/build_details_entity_spec.rb'
+- './spec/serializers/build_trace_entity_spec.rb'
+- './spec/serializers/ci/codequality_mr_diff_entity_spec.rb'
+- './spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb'
+- './spec/serializers/ci/dag_job_entity_spec.rb'
+- './spec/serializers/ci/dag_job_group_entity_spec.rb'
+- './spec/serializers/ci/dag_pipeline_entity_spec.rb'
+- './spec/serializers/ci/dag_pipeline_serializer_spec.rb'
+- './spec/serializers/ci/dag_stage_entity_spec.rb'
+- './spec/serializers/ci/daily_build_group_report_result_entity_spec.rb'
+- './spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb'
+- './spec/serializers/ci/downloadable_artifact_entity_spec.rb'
+- './spec/serializers/ci/downloadable_artifact_serializer_spec.rb'
+- './spec/serializers/ci/group_variable_entity_spec.rb'
+- './spec/serializers/ci/job_entity_spec.rb'
+- './spec/serializers/ci/job_serializer_spec.rb'
+- './spec/serializers/ci/lint/job_entity_spec.rb'
+- './spec/serializers/ci/lint/result_entity_spec.rb'
+- './spec/serializers/ci/lint/result_serializer_spec.rb'
+- './spec/serializers/ci/pipeline_entity_spec.rb'
+- './spec/serializers/ci/trigger_entity_spec.rb'
+- './spec/serializers/ci/trigger_serializer_spec.rb'
+- './spec/serializers/ci/variable_entity_spec.rb'
+- './spec/serializers/cluster_application_entity_spec.rb'
+- './spec/serializers/cluster_entity_spec.rb'
+- './spec/serializers/cluster_serializer_spec.rb'
+- './spec/serializers/clusters/kubernetes_error_entity_spec.rb'
+- './spec/serializers/codequality_degradation_entity_spec.rb'
+- './spec/serializers/codequality_reports_comparer_entity_spec.rb'
+- './spec/serializers/codequality_reports_comparer_serializer_spec.rb'
+- './spec/serializers/commit_entity_spec.rb'
+- './spec/serializers/container_repositories_serializer_spec.rb'
+- './spec/serializers/container_repository_entity_spec.rb'
+- './spec/serializers/container_tag_entity_spec.rb'
+- './spec/serializers/context_commits_diff_entity_spec.rb'
+- './spec/serializers/deploy_keys/basic_deploy_key_entity_spec.rb'
+- './spec/serializers/deploy_keys/deploy_key_entity_spec.rb'
+- './spec/serializers/deployment_cluster_entity_spec.rb'
+- './spec/serializers/deployment_entity_spec.rb'
+- './spec/serializers/deployment_serializer_spec.rb'
+- './spec/serializers/detailed_status_entity_spec.rb'
+- './spec/serializers/diff_file_base_entity_spec.rb'
+- './spec/serializers/diff_file_entity_spec.rb'
+- './spec/serializers/diff_file_metadata_entity_spec.rb'
+- './spec/serializers/diff_line_entity_spec.rb'
+- './spec/serializers/diff_line_serializer_spec.rb'
+- './spec/serializers/diffs_entity_spec.rb'
+- './spec/serializers/diffs_metadata_entity_spec.rb'
+- './spec/serializers/diff_viewer_entity_spec.rb'
+- './spec/serializers/discussion_diff_file_entity_spec.rb'
+- './spec/serializers/discussion_entity_spec.rb'
+- './spec/serializers/entity_date_helper_spec.rb'
+- './spec/serializers/entity_request_spec.rb'
+- './spec/serializers/environment_entity_spec.rb'
+- './spec/serializers/environment_serializer_spec.rb'
+- './spec/serializers/environment_status_entity_spec.rb'
+- './spec/serializers/evidences/evidence_entity_spec.rb'
+- './spec/serializers/evidences/evidence_serializer_spec.rb'
+- './spec/serializers/evidences/issue_entity_spec.rb'
+- './spec/serializers/evidences/milestone_entity_spec.rb'
+- './spec/serializers/evidences/project_entity_spec.rb'
+- './spec/serializers/evidences/release_entity_spec.rb'
+- './spec/serializers/evidences/release_serializer_spec.rb'
+- './spec/serializers/feature_flag_entity_spec.rb'
+- './spec/serializers/feature_flags_client_serializer_spec.rb'
+- './spec/serializers/feature_flag_serializer_spec.rb'
+- './spec/serializers/feature_flag_summary_entity_spec.rb'
+- './spec/serializers/feature_flag_summary_serializer_spec.rb'
+- './spec/serializers/fork_namespace_entity_spec.rb'
+- './spec/serializers/fork_namespace_serializer_spec.rb'
+- './spec/serializers/group_access_token_entity_spec.rb'
+- './spec/serializers/group_access_token_serializer_spec.rb'
+- './spec/serializers/group_child_entity_spec.rb'
+- './spec/serializers/group_child_serializer_spec.rb'
+- './spec/serializers/group_deploy_key_entity_spec.rb'
+- './spec/serializers/group_issuable_autocomplete_entity_spec.rb'
+- './spec/serializers/group_link/group_group_link_entity_spec.rb'
+- './spec/serializers/group_link/group_group_link_serializer_spec.rb'
+- './spec/serializers/group_link/group_link_entity_spec.rb'
+- './spec/serializers/group_link/project_group_link_entity_spec.rb'
+- './spec/serializers/group_link/project_group_link_serializer_spec.rb'
+- './spec/serializers/import/bitbucket_provider_repo_entity_spec.rb'
+- './spec/serializers/import/bitbucket_server_provider_repo_entity_spec.rb'
+- './spec/serializers/import/bulk_import_entity_spec.rb'
+- './spec/serializers/import/fogbugz_provider_repo_entity_spec.rb'
+- './spec/serializers/import/githubish_provider_repo_entity_spec.rb'
+- './spec/serializers/import/gitlab_provider_repo_entity_spec.rb'
+- './spec/serializers/import/manifest_provider_repo_entity_spec.rb'
+- './spec/serializers/import/provider_repo_serializer_spec.rb'
+- './spec/serializers/integrations/event_entity_spec.rb'
+- './spec/serializers/integrations/field_entity_spec.rb'
+- './spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb'
+- './spec/serializers/integrations/harbor_serializers/artifact_serializer_spec.rb'
+- './spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb'
+- './spec/serializers/integrations/harbor_serializers/repository_serializer_spec.rb'
+- './spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb'
+- './spec/serializers/integrations/harbor_serializers/tag_serializer_spec.rb'
+- './spec/serializers/integrations/project_entity_spec.rb'
+- './spec/serializers/integrations/project_serializer_spec.rb'
+- './spec/serializers/issuable_sidebar_extras_entity_spec.rb'
+- './spec/serializers/issue_board_entity_spec.rb'
+- './spec/serializers/issue_entity_spec.rb'
+- './spec/serializers/issue_serializer_spec.rb'
+- './spec/serializers/issue_sidebar_basic_entity_spec.rb'
+- './spec/serializers/jira_connect/app_data_serializer_spec.rb'
+- './spec/serializers/jira_connect/group_entity_spec.rb'
+- './spec/serializers/jira_connect/subscription_entity_spec.rb'
+- './spec/serializers/job_artifact_report_entity_spec.rb'
+- './spec/serializers/label_serializer_spec.rb'
+- './spec/serializers/lfs_file_lock_entity_spec.rb'
+- './spec/serializers/linked_project_issue_entity_spec.rb'
+- './spec/serializers/member_entity_spec.rb'
+- './spec/serializers/member_serializer_spec.rb'
+- './spec/serializers/member_user_entity_spec.rb'
+- './spec/serializers/merge_request_basic_entity_spec.rb'
+- './spec/serializers/merge_request_current_user_entity_spec.rb'
+- './spec/serializers/merge_request_diff_entity_spec.rb'
+- './spec/serializers/merge_request_for_pipeline_entity_spec.rb'
+- './spec/serializers/merge_request_metrics_helper_spec.rb'
+- './spec/serializers/merge_request_poll_cached_widget_entity_spec.rb'
+- './spec/serializers/merge_request_poll_widget_entity_spec.rb'
+- './spec/serializers/merge_request_serializer_spec.rb'
+- './spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
+- './spec/serializers/merge_request_sidebar_extras_entity_spec.rb'
+- './spec/serializers/merge_requests/pipeline_entity_spec.rb'
+- './spec/serializers/merge_request_user_entity_spec.rb'
+- './spec/serializers/merge_request_widget_commit_entity_spec.rb'
+- './spec/serializers/merge_request_widget_entity_spec.rb'
+- './spec/serializers/move_to_project_entity_spec.rb'
+- './spec/serializers/move_to_project_serializer_spec.rb'
+- './spec/serializers/namespace_basic_entity_spec.rb'
+- './spec/serializers/namespace_serializer_spec.rb'
+- './spec/serializers/note_entity_spec.rb'
+- './spec/serializers/paginated_diff_entity_spec.rb'
+- './spec/serializers/personal_access_token_entity_spec.rb'
+- './spec/serializers/personal_access_token_serializer_spec.rb'
+- './spec/serializers/pipeline_details_entity_spec.rb'
+- './spec/serializers/pipeline_serializer_spec.rb'
+- './spec/serializers/project_access_token_entity_spec.rb'
+- './spec/serializers/project_access_token_serializer_spec.rb'
+- './spec/serializers/project_import_entity_spec.rb'
+- './spec/serializers/project_mirror_entity_spec.rb'
+- './spec/serializers/project_mirror_serializer_spec.rb'
+- './spec/serializers/project_note_entity_spec.rb'
+- './spec/serializers/project_serializer_spec.rb'
+- './spec/serializers/prometheus_alert_entity_spec.rb'
+- './spec/serializers/release_serializer_spec.rb'
+- './spec/serializers/remote_mirror_entity_spec.rb'
+- './spec/serializers/request_aware_entity_spec.rb'
+- './spec/serializers/review_app_setup_entity_spec.rb'
+- './spec/serializers/rollout_status_entity_spec.rb'
+- './spec/serializers/rollout_statuses/ingress_entity_spec.rb'
+- './spec/serializers/runner_entity_spec.rb'
+- './spec/serializers/serverless/domain_entity_spec.rb'
+- './spec/serializers/stage_entity_spec.rb'
+- './spec/serializers/stage_serializer_spec.rb'
+- './spec/serializers/suggestion_entity_spec.rb'
+- './spec/serializers/test_case_entity_spec.rb'
+- './spec/serializers/test_report_entity_spec.rb'
+- './spec/serializers/test_reports_comparer_entity_spec.rb'
+- './spec/serializers/test_reports_comparer_serializer_spec.rb'
+- './spec/serializers/test_report_summary_entity_spec.rb'
+- './spec/serializers/test_suite_comparer_entity_spec.rb'
+- './spec/serializers/test_suite_entity_spec.rb'
+- './spec/serializers/test_suite_summary_entity_spec.rb'
+- './spec/serializers/trigger_variable_entity_spec.rb'
+- './spec/serializers/user_entity_spec.rb'
+- './spec/serializers/user_serializer_spec.rb'
+- './spec/serializers/web_ide_terminal_entity_spec.rb'
+- './spec/serializers/web_ide_terminal_serializer_spec.rb'
+- './spec/services/access_token_validation_service_spec.rb'
+- './spec/services/alert_management/alerts/todo/create_service_spec.rb'
+- './spec/services/alert_management/alerts/update_service_spec.rb'
+- './spec/services/alert_management/create_alert_issue_service_spec.rb'
+- './spec/services/alert_management/http_integrations/create_service_spec.rb'
+- './spec/services/alert_management/http_integrations/destroy_service_spec.rb'
+- './spec/services/alert_management/http_integrations/update_service_spec.rb'
+- './spec/services/alert_management/metric_images/upload_service_spec.rb'
+- './spec/services/alert_management/process_prometheus_alert_service_spec.rb'
+- './spec/services/analytics/cycle_analytics/stages/list_service_spec.rb'
+- './spec/services/applications/create_service_spec.rb'
+- './spec/services/application_settings/update_service_spec.rb'
+- './spec/services/audit_events/build_service_spec.rb'
+- './spec/services/audit_event_service_spec.rb'
+- './spec/services/auth/container_registry_authentication_service_spec.rb'
+- './spec/services/auth/dependency_proxy_authentication_service_spec.rb'
+- './spec/services/authorized_project_update/find_records_due_for_refresh_service_spec.rb'
+- './spec/services/authorized_project_update/periodic_recalculate_service_spec.rb'
+- './spec/services/authorized_project_update/project_access_changed_service_spec.rb'
+- './spec/services/authorized_project_update/project_recalculate_per_user_service_spec.rb'
+- './spec/services/authorized_project_update/project_recalculate_service_spec.rb'
+- './spec/services/auto_merge/base_service_spec.rb'
+- './spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb'
+- './spec/services/auto_merge_service_spec.rb'
+- './spec/services/award_emojis/add_service_spec.rb'
+- './spec/services/award_emojis/base_service_spec.rb'
+- './spec/services/award_emojis/collect_user_emoji_service_spec.rb'
+- './spec/services/award_emojis/copy_service_spec.rb'
+- './spec/services/award_emojis/destroy_service_spec.rb'
+- './spec/services/award_emojis/toggle_service_spec.rb'
+- './spec/services/base_container_service_spec.rb'
+- './spec/services/base_count_service_spec.rb'
+- './spec/services/boards/create_service_spec.rb'
+- './spec/services/boards/destroy_service_spec.rb'
+- './spec/services/boards/issues/create_service_spec.rb'
+- './spec/services/boards/issues/list_service_spec.rb'
+- './spec/services/boards/issues/move_service_spec.rb'
+- './spec/services/boards/lists/create_service_spec.rb'
+- './spec/services/boards/lists/destroy_service_spec.rb'
+- './spec/services/boards/lists/generate_service_spec.rb'
+- './spec/services/boards/lists/list_service_spec.rb'
+- './spec/services/boards/lists/move_service_spec.rb'
+- './spec/services/boards/lists/update_service_spec.rb'
+- './spec/services/boards/visits/create_service_spec.rb'
+- './spec/services/branches/create_service_spec.rb'
+- './spec/services/branches/delete_merged_service_spec.rb'
+- './spec/services/branches/delete_service_spec.rb'
+- './spec/services/branches/diverging_commit_counts_service_spec.rb'
+- './spec/services/branches/validate_new_service_spec.rb'
+- './spec/services/bulk_create_integration_service_spec.rb'
+- './spec/services/bulk_imports/archive_extraction_service_spec.rb'
+- './spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb'
+- './spec/services/bulk_imports/create_service_spec.rb'
+- './spec/services/bulk_imports/export_service_spec.rb'
+- './spec/services/bulk_imports/file_decompression_service_spec.rb'
+- './spec/services/bulk_imports/file_download_service_spec.rb'
+- './spec/services/bulk_imports/file_export_service_spec.rb'
+- './spec/services/bulk_imports/get_importable_data_service_spec.rb'
+- './spec/services/bulk_imports/lfs_objects_export_service_spec.rb'
+- './spec/services/bulk_imports/relation_export_service_spec.rb'
+- './spec/services/bulk_imports/repository_bundle_export_service_spec.rb'
+- './spec/services/bulk_imports/tree_export_service_spec.rb'
+- './spec/services/bulk_imports/uploads_export_service_spec.rb'
+- './spec/services/bulk_push_event_payload_service_spec.rb'
+- './spec/services/bulk_update_integration_service_spec.rb'
+- './spec/services/captcha/captcha_verification_service_spec.rb'
+- './spec/services/chat_names/authorize_user_service_spec.rb'
+- './spec/services/chat_names/find_user_service_spec.rb'
+- './spec/services/ci/abort_pipelines_service_spec.rb'
+- './spec/services/ci/after_requeue_job_service_spec.rb'
+- './spec/services/ci/append_build_trace_service_spec.rb'
+- './spec/services/ci/archive_trace_service_spec.rb'
+- './spec/services/ci/build_cancel_service_spec.rb'
+- './spec/services/ci/build_report_result_service_spec.rb'
+- './spec/services/ci/build_unschedule_service_spec.rb'
+- './spec/services/ci/change_variable_service_spec.rb'
+- './spec/services/ci/change_variables_service_spec.rb'
+- './spec/services/ci/compare_accessibility_reports_service_spec.rb'
+- './spec/services/ci/compare_codequality_reports_service_spec.rb'
+- './spec/services/ci/compare_reports_base_service_spec.rb'
+- './spec/services/ci/compare_test_reports_service_spec.rb'
+- './spec/services/ci/copy_cross_database_associations_service_spec.rb'
+- './spec/services/ci/create_downstream_pipeline_service_spec.rb'
+- './spec/services/ci/create_pipeline_service/artifacts_spec.rb'
+- './spec/services/ci/create_pipeline_service/cache_spec.rb'
+- './spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb'
+- './spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb'
+- './spec/services/ci/create_pipeline_service/custom_config_content_spec.rb'
+- './spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb'
+- './spec/services/ci/create_pipeline_service/dry_run_spec.rb'
+- './spec/services/ci/create_pipeline_service/environment_spec.rb'
+- './spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb'
+- './spec/services/ci/create_pipeline_service/include_spec.rb'
+- './spec/services/ci/create_pipeline_service/logger_spec.rb'
+- './spec/services/ci/create_pipeline_service/merge_requests_spec.rb'
+- './spec/services/ci/create_pipeline_service/needs_spec.rb'
+- './spec/services/ci/create_pipeline_service/parallel_spec.rb'
+- './spec/services/ci/create_pipeline_service/parameter_content_spec.rb'
+- './spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb'
+- './spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb'
+- './spec/services/ci/create_pipeline_service/rate_limit_spec.rb'
+- './spec/services/ci/create_pipeline_service/rules_spec.rb'
+- './spec/services/ci/create_pipeline_service_spec.rb'
+- './spec/services/ci/create_pipeline_service/tags_spec.rb'
+- './spec/services/ci/create_web_ide_terminal_service_spec.rb'
+- './spec/services/ci/daily_build_group_report_result_service_spec.rb'
+- './spec/services/ci/delete_objects_service_spec.rb'
+- './spec/services/ci/delete_unit_tests_service_spec.rb'
+- './spec/services/ci/deployments/destroy_service_spec.rb'
+- './spec/services/ci/destroy_pipeline_service_spec.rb'
+- './spec/services/ci/destroy_secure_file_service_spec.rb'
+- './spec/services/ci/disable_user_pipeline_schedules_service_spec.rb'
+- './spec/services/ci/drop_pipeline_service_spec.rb'
+- './spec/services/ci/ensure_stage_service_spec.rb'
+- './spec/services/ci/expire_pipeline_cache_service_spec.rb'
+- './spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb'
+- './spec/services/ci/find_exposed_artifacts_service_spec.rb'
+- './spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb'
+- './spec/services/ci/generate_coverage_reports_service_spec.rb'
+- './spec/services/ci/generate_kubeconfig_service_spec.rb'
+- './spec/services/ci/generate_terraform_reports_service_spec.rb'
+- './spec/services/ci/job_artifacts/create_service_spec.rb'
+- './spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb'
+- './spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb'
+- './spec/services/ci/job_artifacts/destroy_associations_service_spec.rb'
+- './spec/services/ci/job_artifacts/destroy_batch_service_spec.rb'
+- './spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb'
+- './spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb'
+- './spec/services/ci/job_token_scope/add_project_service_spec.rb'
+- './spec/services/ci/job_token_scope/remove_project_service_spec.rb'
+- './spec/services/ci/list_config_variables_service_spec.rb'
+- './spec/services/ci/parse_dotenv_artifact_service_spec.rb'
+- './spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb'
+- './spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb'
+- './spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb'
+- './spec/services/ci/pipeline_bridge_status_service_spec.rb'
+- './spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb'
+- './spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb'
+- './spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb'
+- './spec/services/ci/pipelines/add_job_service_spec.rb'
+- './spec/services/ci/pipeline_schedule_service_spec.rb'
+- './spec/services/ci/pipelines/hook_service_spec.rb'
+- './spec/services/ci/pipeline_trigger_service_spec.rb'
+- './spec/services/ci/play_bridge_service_spec.rb'
+- './spec/services/ci/play_build_service_spec.rb'
+- './spec/services/ci/play_manual_stage_service_spec.rb'
+- './spec/services/ci/prepare_build_service_spec.rb'
+- './spec/services/ci/process_build_service_spec.rb'
+- './spec/services/ci/process_pipeline_service_spec.rb'
+- './spec/services/ci/process_sync_events_service_spec.rb'
+- './spec/services/ci/prometheus_metrics/observe_histograms_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_job_service_spec.rb'
+- './spec/services/ci/retry_pipeline_service_spec.rb'
+- './spec/services/ci/runners/assign_runner_service_spec.rb'
+- './spec/services/ci/runners/bulk_delete_runners_service_spec.rb'
+- './spec/services/ci/runners/process_runner_version_update_service_spec.rb'
+- './spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb'
+- './spec/services/ci/runners/register_runner_service_spec.rb'
+- './spec/services/ci/runners/reset_registration_token_service_spec.rb'
+- './spec/services/ci/runners/unassign_runner_service_spec.rb'
+- './spec/services/ci/runners/unregister_runner_service_spec.rb'
+- './spec/services/ci/runners/update_runner_service_spec.rb'
+- './spec/services/ci/run_scheduled_build_service_spec.rb'
+- './spec/services/ci/stuck_builds/drop_pending_service_spec.rb'
+- './spec/services/ci/stuck_builds/drop_running_service_spec.rb'
+- './spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb'
+- './spec/services/ci/test_failure_history_service_spec.rb'
+- './spec/services/ci/track_failed_build_service_spec.rb'
+- './spec/services/ci/unlock_artifacts_service_spec.rb'
+- './spec/services/ci/update_build_queue_service_spec.rb'
+- './spec/services/ci/update_build_state_service_spec.rb'
+- './spec/services/ci/update_instance_variables_service_spec.rb'
+- './spec/services/ci/update_pending_build_service_spec.rb'
+- './spec/services/clusters/agents/create_activity_event_service_spec.rb'
+- './spec/services/clusters/agents/create_service_spec.rb'
+- './spec/services/clusters/agents/delete_expired_events_service_spec.rb'
+- './spec/services/clusters/agents/delete_service_spec.rb'
+- './spec/services/clusters/agents/refresh_authorization_service_spec.rb'
+- './spec/services/clusters/agent_tokens/create_service_spec.rb'
+- './spec/services/clusters/agent_tokens/track_usage_service_spec.rb'
+- './spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb'
+- './spec/services/clusters/applications/check_installation_progress_service_spec.rb'
+- './spec/services/clusters/applications/check_uninstall_progress_service_spec.rb'
+- './spec/services/clusters/applications/check_upgrade_progress_service_spec.rb'
+- './spec/services/clusters/applications/create_service_spec.rb'
+- './spec/services/clusters/applications/destroy_service_spec.rb'
+- './spec/services/clusters/applications/install_service_spec.rb'
+- './spec/services/clusters/applications/patch_service_spec.rb'
+- './spec/services/clusters/applications/prometheus_config_service_spec.rb'
+- './spec/services/clusters/applications/prometheus_update_service_spec.rb'
+- './spec/services/clusters/applications/uninstall_service_spec.rb'
+- './spec/services/clusters/applications/update_service_spec.rb'
+- './spec/services/clusters/applications/upgrade_service_spec.rb'
+- './spec/services/clusters/aws/authorize_role_service_spec.rb'
+- './spec/services/clusters/aws/fetch_credentials_service_spec.rb'
+- './spec/services/clusters/aws/finalize_creation_service_spec.rb'
+- './spec/services/clusters/aws/provision_service_spec.rb'
+- './spec/services/clusters/aws/verify_provision_status_service_spec.rb'
+- './spec/services/clusters/build_kubernetes_namespace_service_spec.rb'
+- './spec/services/clusters/build_service_spec.rb'
+- './spec/services/clusters/cleanup/project_namespace_service_spec.rb'
+- './spec/services/clusters/cleanup/service_account_service_spec.rb'
+- './spec/services/clusters/create_service_spec.rb'
+- './spec/services/clusters/destroy_service_spec.rb'
+- './spec/services/clusters/gcp/fetch_operation_service_spec.rb'
+- './spec/services/clusters/gcp/finalize_creation_service_spec.rb'
+- './spec/services/clusters/gcp/provision_service_spec.rb'
+- './spec/services/clusters/gcp/verify_provision_status_service_spec.rb'
+- './spec/services/clusters/integrations/create_service_spec.rb'
+- './spec/services/clusters/integrations/prometheus_health_check_service_spec.rb'
+- './spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb'
+- './spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb'
+- './spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb'
+- './spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb'
+- './spec/services/clusters/kubernetes_spec.rb'
+- './spec/services/clusters/management/validate_management_project_permissions_service_spec.rb'
+- './spec/services/clusters/update_service_spec.rb'
+- './spec/services/cohorts_service_spec.rb'
+- './spec/services/commits/cherry_pick_service_spec.rb'
+- './spec/services/commits/commit_patch_service_spec.rb'
+- './spec/services/commits/tag_service_spec.rb'
+- './spec/services/compare_service_spec.rb'
+- './spec/services/concerns/audit_event_save_type_spec.rb'
+- './spec/services/concerns/exclusive_lease_guard_spec.rb'
+- './spec/services/concerns/merge_requests/assigns_merge_params_spec.rb'
+- './spec/services/concerns/rate_limited_service_spec.rb'
+- './spec/services/container_expiration_policies/cleanup_service_spec.rb'
+- './spec/services/container_expiration_policies/update_service_spec.rb'
+- './spec/services/customer_relations/contacts/create_service_spec.rb'
+- './spec/services/customer_relations/contacts/update_service_spec.rb'
+- './spec/services/customer_relations/organizations/create_service_spec.rb'
+- './spec/services/customer_relations/organizations/update_service_spec.rb'
+- './spec/services/database/consistency_check_service_spec.rb'
+- './spec/services/database/consistency_fix_service_spec.rb'
+- './spec/services/dependency_proxy/auth_token_service_spec.rb'
+- './spec/services/dependency_proxy/find_cached_manifest_service_spec.rb'
+- './spec/services/dependency_proxy/group_settings/update_service_spec.rb'
+- './spec/services/dependency_proxy/head_manifest_service_spec.rb'
+- './spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb'
+- './spec/services/dependency_proxy/request_token_service_spec.rb'
+- './spec/services/deploy_keys/create_service_spec.rb'
+- './spec/services/deployments/archive_in_project_service_spec.rb'
+- './spec/services/deployments/create_for_build_service_spec.rb'
+- './spec/services/deployments/create_service_spec.rb'
+- './spec/services/deployments/link_merge_requests_service_spec.rb'
+- './spec/services/deployments/older_deployments_drop_service_spec.rb'
+- './spec/services/deployments/update_environment_service_spec.rb'
+- './spec/services/deployments/update_service_spec.rb'
+- './spec/services/design_management/copy_design_collection/copy_service_spec.rb'
+- './spec/services/design_management/copy_design_collection/queue_service_spec.rb'
+- './spec/services/design_management/delete_designs_service_spec.rb'
+- './spec/services/design_management/design_user_notes_count_service_spec.rb'
+- './spec/services/design_management/generate_image_versions_service_spec.rb'
+- './spec/services/design_management/move_designs_service_spec.rb'
+- './spec/services/design_management/save_designs_service_spec.rb'
+- './spec/services/discussions/capture_diff_note_position_service_spec.rb'
+- './spec/services/discussions/capture_diff_note_positions_service_spec.rb'
+- './spec/services/discussions/resolve_service_spec.rb'
+- './spec/services/discussions/unresolve_service_spec.rb'
+- './spec/services/discussions/update_diff_position_service_spec.rb'
+- './spec/services/draft_notes/create_service_spec.rb'
+- './spec/services/draft_notes/destroy_service_spec.rb'
+- './spec/services/draft_notes/publish_service_spec.rb'
+- './spec/services/emails/confirm_service_spec.rb'
+- './spec/services/emails/create_service_spec.rb'
+- './spec/services/emails/destroy_service_spec.rb'
+- './spec/services/environments/auto_stop_service_spec.rb'
+- './spec/services/environments/canary_ingress/update_service_spec.rb'
+- './spec/services/environments/reset_auto_stop_service_spec.rb'
+- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
+- './spec/services/environments/stop_service_spec.rb'
+- './spec/services/error_tracking/base_service_spec.rb'
+- './spec/services/error_tracking/collect_error_service_spec.rb'
+- './spec/services/error_tracking/issue_details_service_spec.rb'
+- './spec/services/error_tracking/issue_latest_event_service_spec.rb'
+- './spec/services/error_tracking/issue_update_service_spec.rb'
+- './spec/services/error_tracking/list_issues_service_spec.rb'
+- './spec/services/error_tracking/list_projects_service_spec.rb'
+- './spec/services/event_create_service_spec.rb'
+- './spec/services/events/destroy_service_spec.rb'
+- './spec/services/events/render_service_spec.rb'
+- './spec/services/feature_flags/create_service_spec.rb'
+- './spec/services/feature_flags/destroy_service_spec.rb'
+- './spec/services/feature_flags/hook_service_spec.rb'
+- './spec/services/feature_flags/update_service_spec.rb'
+- './spec/services/files/create_service_spec.rb'
+- './spec/services/files/delete_service_spec.rb'
+- './spec/services/files/multi_service_spec.rb'
+- './spec/services/files/update_service_spec.rb'
+- './spec/services/git/base_hooks_service_spec.rb'
+- './spec/services/git/branch_hooks_service_spec.rb'
+- './spec/services/git/branch_push_service_spec.rb'
+- './spec/services/git/process_ref_changes_service_spec.rb'
+- './spec/services/git/tag_hooks_service_spec.rb'
+- './spec/services/git/tag_push_service_spec.rb'
+- './spec/services/git/wiki_push_service/change_spec.rb'
+- './spec/services/git/wiki_push_service_spec.rb'
+- './spec/services/google_cloud/create_cloudsql_instance_service_spec.rb'
+- './spec/services/google_cloud/create_service_accounts_service_spec.rb'
+- './spec/services/google_cloud/enable_cloud_run_service_spec.rb'
+- './spec/services/google_cloud/enable_cloudsql_service_spec.rb'
+- './spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb'
+- './spec/services/google_cloud/generate_pipeline_service_spec.rb'
+- './spec/services/google_cloud/get_cloudsql_instances_service_spec.rb'
+- './spec/services/google_cloud/service_accounts_service_spec.rb'
+- './spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb'
+- './spec/services/gpg_keys/create_service_spec.rb'
+- './spec/services/gpg_keys/destroy_service_spec.rb'
+- './spec/services/grafana/proxy_service_spec.rb'
+- './spec/services/gravatar_service_spec.rb'
+- './spec/services/groups/autocomplete_service_spec.rb'
+- './spec/services/groups/auto_devops_service_spec.rb'
+- './spec/services/groups/create_service_spec.rb'
+- './spec/services/groups/deploy_tokens/create_service_spec.rb'
+- './spec/services/groups/deploy_tokens/destroy_service_spec.rb'
+- './spec/services/groups/deploy_tokens/revoke_service_spec.rb'
+- './spec/services/groups/destroy_service_spec.rb'
+- './spec/services/groups/group_links/create_service_spec.rb'
+- './spec/services/groups/group_links/destroy_service_spec.rb'
+- './spec/services/groups/group_links/update_service_spec.rb'
+- './spec/services/groups/import_export/export_service_spec.rb'
+- './spec/services/groups/import_export/import_service_spec.rb'
+- './spec/services/groups/merge_requests_count_service_spec.rb'
+- './spec/services/groups/nested_create_service_spec.rb'
+- './spec/services/groups/open_issues_count_service_spec.rb'
+- './spec/services/groups/participants_service_spec.rb'
+- './spec/services/groups/transfer_service_spec.rb'
+- './spec/services/groups/update_service_spec.rb'
+- './spec/services/groups/update_shared_runners_service_spec.rb'
+- './spec/services/groups/update_statistics_service_spec.rb'
+- './spec/services/ide/base_config_service_spec.rb'
+- './spec/services/ide/schemas_config_service_spec.rb'
+- './spec/services/ide/terminal_config_service_spec.rb'
+- './spec/services/import/bitbucket_server_service_spec.rb'
+- './spec/services/import_export_clean_up_service_spec.rb'
+- './spec/services/import/fogbugz_service_spec.rb'
+- './spec/services/import/github/notes/create_service_spec.rb'
+- './spec/services/import/github_service_spec.rb'
+- './spec/services/import/gitlab_projects/create_project_service_spec.rb'
+- './spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb'
+- './spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb'
+- './spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb'
+- './spec/services/import/prepare_service_spec.rb'
+- './spec/services/import/validate_remote_git_endpoint_service_spec.rb'
+- './spec/services/incident_management/incidents/create_service_spec.rb'
+- './spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb'
+- './spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb'
+- './spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb'
+- './spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb'
+- './spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb'
+- './spec/services/incident_management/pager_duty/process_webhook_service_spec.rb'
+- './spec/services/incident_management/timeline_events/create_service_spec.rb'
+- './spec/services/incident_management/timeline_events/destroy_service_spec.rb'
+- './spec/services/incident_management/timeline_events/update_service_spec.rb'
+- './spec/services/integrations/propagate_service_spec.rb'
+- './spec/services/integrations/test/project_service_spec.rb'
+- './spec/services/issuable/bulk_update_service_spec.rb'
+- './spec/services/issuable/common_system_notes_service_spec.rb'
+- './spec/services/issuable/destroy_label_links_service_spec.rb'
+- './spec/services/issuable/destroy_service_spec.rb'
+- './spec/services/issuable/process_assignees_spec.rb'
+- './spec/services/issue_links/create_service_spec.rb'
+- './spec/services/issue_links/destroy_service_spec.rb'
+- './spec/services/issue_links/list_service_spec.rb'
+- './spec/services/issues/after_create_service_spec.rb'
+- './spec/services/issues/build_service_spec.rb'
+- './spec/services/issues/clone_service_spec.rb'
+- './spec/services/issues/close_service_spec.rb'
+- './spec/services/issues/create_service_spec.rb'
+- './spec/services/issues/duplicate_service_spec.rb'
+- './spec/services/issues/export_csv_service_spec.rb'
+- './spec/services/issues/import_csv_service_spec.rb'
+- './spec/services/issues/move_service_spec.rb'
+- './spec/services/issues/prepare_import_csv_service_spec.rb'
+- './spec/services/issues/referenced_merge_requests_service_spec.rb'
+- './spec/services/issues/related_branches_service_spec.rb'
+- './spec/services/issues/relative_position_rebalancing_service_spec.rb'
+- './spec/services/issues/reopen_service_spec.rb'
+- './spec/services/issues/reorder_service_spec.rb'
+- './spec/services/issues/resolve_discussions_spec.rb'
+- './spec/services/issues/set_crm_contacts_service_spec.rb'
+- './spec/services/issues/update_service_spec.rb'
+- './spec/services/issues/zoom_link_service_spec.rb'
+- './spec/services/jira_connect_installations/destroy_service_spec.rb'
+- './spec/services/jira_connect_subscriptions/create_service_spec.rb'
+- './spec/services/jira_connect/sync_service_spec.rb'
+- './spec/services/jira_import/cloud_users_mapper_service_spec.rb'
+- './spec/services/jira_import/server_users_mapper_service_spec.rb'
+- './spec/services/jira_import/start_import_service_spec.rb'
+- './spec/services/jira_import/users_importer_spec.rb'
+- './spec/services/jira/requests/projects/list_service_spec.rb'
+- './spec/services/keys/create_service_spec.rb'
+- './spec/services/keys/destroy_service_spec.rb'
+- './spec/services/keys/expiry_notification_service_spec.rb'
+- './spec/services/keys/last_used_service_spec.rb'
+- './spec/services/labels/available_labels_service_spec.rb'
+- './spec/services/labels/create_service_spec.rb'
+- './spec/services/labels/find_or_create_service_spec.rb'
+- './spec/services/labels/promote_service_spec.rb'
+- './spec/services/labels/transfer_service_spec.rb'
+- './spec/services/labels/update_service_spec.rb'
+- './spec/services/lfs/file_transformer_spec.rb'
+- './spec/services/lfs/lock_file_service_spec.rb'
+- './spec/services/lfs/locks_finder_service_spec.rb'
+- './spec/services/lfs/push_service_spec.rb'
+- './spec/services/lfs/unlock_file_service_spec.rb'
+- './spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb'
+- './spec/services/loose_foreign_keys/cleaner_service_spec.rb'
+- './spec/services/markdown_content_rewriter_service_spec.rb'
+- './spec/services/members/approve_access_request_service_spec.rb'
+- './spec/services/members/create_service_spec.rb'
+- './spec/services/members/creator_service_spec.rb'
+- './spec/services/members/destroy_service_spec.rb'
+- './spec/services/members/groups/creator_service_spec.rb'
+- './spec/services/members/import_project_team_service_spec.rb'
+- './spec/services/members/invitation_reminder_email_service_spec.rb'
+- './spec/services/members/invite_member_builder_spec.rb'
+- './spec/services/members/invite_service_spec.rb'
+- './spec/services/members/projects/creator_service_spec.rb'
+- './spec/services/members/request_access_service_spec.rb'
+- './spec/services/members/standard_member_builder_spec.rb'
+- './spec/services/members/unassign_issuables_service_spec.rb'
+- './spec/services/members/update_service_spec.rb'
+- './spec/services/merge_requests/add_context_service_spec.rb'
+- './spec/services/merge_requests/add_spent_time_service_spec.rb'
+- './spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
+- './spec/services/merge_requests/after_create_service_spec.rb'
+- './spec/services/merge_requests/approval_service_spec.rb'
+- './spec/services/merge_requests/assign_issues_service_spec.rb'
+- './spec/services/merge_requests/base_service_spec.rb'
+- './spec/services/merge_requests/build_service_spec.rb'
+- './spec/services/merge_requests/cleanup_refs_service_spec.rb'
+- './spec/services/merge_requests/close_service_spec.rb'
+- './spec/services/merge_requests/conflicts/list_service_spec.rb'
+- './spec/services/merge_requests/conflicts/resolve_service_spec.rb'
+- './spec/services/merge_requests/create_approval_event_service_spec.rb'
+- './spec/services/merge_requests/create_from_issue_service_spec.rb'
+- './spec/services/merge_requests/create_pipeline_service_spec.rb'
+- './spec/services/merge_requests/create_service_spec.rb'
+- './spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb'
+- './spec/services/merge_requests/execute_approval_hooks_service_spec.rb'
+- './spec/services/merge_requests/export_csv_service_spec.rb'
+- './spec/services/merge_requests/ff_merge_service_spec.rb'
+- './spec/services/merge_requests/get_urls_service_spec.rb'
+- './spec/services/merge_requests/handle_assignees_change_service_spec.rb'
+- './spec/services/merge_requests/link_lfs_objects_service_spec.rb'
+- './spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb'
+- './spec/services/merge_requests/mergeability/check_base_service_spec.rb'
+- './spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb'
+- './spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb'
+- './spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb'
+- './spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb'
+- './spec/services/merge_requests/mergeability/check_open_status_service_spec.rb'
+- './spec/services/merge_requests/mergeability_check_service_spec.rb'
+- './spec/services/merge_requests/mergeability/run_checks_service_spec.rb'
+- './spec/services/merge_requests/merge_orchestration_service_spec.rb'
+- './spec/services/merge_requests/merge_service_spec.rb'
+- './spec/services/merge_requests/merge_to_ref_service_spec.rb'
+- './spec/services/merge_requests/migrate_external_diffs_service_spec.rb'
+- './spec/services/merge_requests/post_merge_service_spec.rb'
+- './spec/services/merge_requests/pushed_branches_service_spec.rb'
+- './spec/services/merge_requests/push_options_handler_service_spec.rb'
+- './spec/services/merge_requests/rebase_service_spec.rb'
+- './spec/services/merge_requests/refresh_service_spec.rb'
+- './spec/services/merge_requests/reload_diffs_service_spec.rb'
+- './spec/services/merge_requests/reload_merge_head_diff_service_spec.rb'
+- './spec/services/merge_requests/remove_approval_service_spec.rb'
+- './spec/services/merge_requests/reopen_service_spec.rb'
+- './spec/services/merge_requests/request_review_service_spec.rb'
+- './spec/services/merge_requests/resolved_discussion_notification_service_spec.rb'
+- './spec/services/merge_requests/resolve_todos_service_spec.rb'
+- './spec/services/merge_requests/retarget_chain_service_spec.rb'
+- './spec/services/merge_requests/squash_service_spec.rb'
+- './spec/services/merge_requests/update_assignees_service_spec.rb'
+- './spec/services/merge_requests/update_reviewers_service_spec.rb'
+- './spec/services/merge_requests/update_service_spec.rb'
+- './spec/services/metrics/dashboard/annotations/create_service_spec.rb'
+- './spec/services/metrics/dashboard/annotations/delete_service_spec.rb'
+- './spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
+- './spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb'
+- './spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
+- './spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/default_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/panel_preview_service_spec.rb'
+- './spec/services/metrics/dashboard/pod_dashboard_service_spec.rb'
+- './spec/services/metrics/dashboard/self_monitoring_dashboard_service_spec.rb'
+- './spec/services/metrics/dashboard/system_dashboard_service_spec.rb'
+- './spec/services/metrics/dashboard/transient_embed_service_spec.rb'
+- './spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
+- './spec/services/metrics/sample_metrics_service_spec.rb'
+- './spec/services/metrics/users_starred_dashboards/create_service_spec.rb'
+- './spec/services/metrics/users_starred_dashboards/delete_service_spec.rb'
+- './spec/services/milestones/closed_issues_count_service_spec.rb'
+- './spec/services/milestones/close_service_spec.rb'
+- './spec/services/milestones/create_service_spec.rb'
+- './spec/services/milestones/destroy_service_spec.rb'
+- './spec/services/milestones/find_or_create_service_spec.rb'
+- './spec/services/milestones/issues_count_service_spec.rb'
+- './spec/services/milestones/merge_requests_count_service_spec.rb'
+- './spec/services/milestones/promote_service_spec.rb'
+- './spec/services/milestones/transfer_service_spec.rb'
+- './spec/services/milestones/update_service_spec.rb'
+- './spec/services/namespace_settings/update_service_spec.rb'
+- './spec/services/namespaces/in_product_marketing_emails_service_spec.rb'
+- './spec/services/namespaces/package_settings/update_service_spec.rb'
+- './spec/services/namespaces/statistics_refresher_service_spec.rb'
+- './spec/services/notes/build_service_spec.rb'
+- './spec/services/notes/copy_service_spec.rb'
+- './spec/services/notes/create_service_spec.rb'
+- './spec/services/notes/destroy_service_spec.rb'
+- './spec/services/notes/post_process_service_spec.rb'
+- './spec/services/notes/quick_actions_service_spec.rb'
+- './spec/services/notes/render_service_spec.rb'
+- './spec/services/notes/resolve_service_spec.rb'
+- './spec/services/note_summary_spec.rb'
+- './spec/services/notes/update_service_spec.rb'
+- './spec/services/notification_recipients/builder/default_spec.rb'
+- './spec/services/notification_recipients/builder/new_note_spec.rb'
+- './spec/services/notification_recipients/build_service_spec.rb'
+- './spec/services/notification_service_spec.rb'
+- './spec/services/packages/cleanup/execute_policy_service_spec.rb'
+- './spec/services/packages/cleanup/update_policy_service_spec.rb'
+- './spec/services/packages/composer/composer_json_service_spec.rb'
+- './spec/services/packages/composer/create_package_service_spec.rb'
+- './spec/services/packages/composer/version_parser_service_spec.rb'
+- './spec/services/packages/conan/create_package_file_service_spec.rb'
+- './spec/services/packages/conan/create_package_service_spec.rb'
+- './spec/services/packages/conan/search_service_spec.rb'
+- './spec/services/packages/create_dependency_service_spec.rb'
+- './spec/services/packages/create_event_service_spec.rb'
+- './spec/services/packages/create_package_file_service_spec.rb'
+- './spec/services/packages/create_temporary_package_service_spec.rb'
+- './spec/services/packages/debian/create_distribution_service_spec.rb'
+- './spec/services/packages/debian/create_package_file_service_spec.rb'
+- './spec/services/packages/debian/extract_changes_metadata_service_spec.rb'
+- './spec/services/packages/debian/extract_deb_metadata_service_spec.rb'
+- './spec/services/packages/debian/extract_metadata_service_spec.rb'
+- './spec/services/packages/debian/find_or_create_incoming_service_spec.rb'
+- './spec/services/packages/debian/find_or_create_package_service_spec.rb'
+- './spec/services/packages/debian/generate_distribution_key_service_spec.rb'
+- './spec/services/packages/debian/generate_distribution_service_spec.rb'
+- './spec/services/packages/debian/parse_debian822_service_spec.rb'
+- './spec/services/packages/debian/process_changes_service_spec.rb'
+- './spec/services/packages/debian/sign_distribution_service_spec.rb'
+- './spec/services/packages/debian/update_distribution_service_spec.rb'
+- './spec/services/packages/generic/create_package_file_service_spec.rb'
+- './spec/services/packages/generic/find_or_create_package_service_spec.rb'
+- './spec/services/packages/go/create_package_service_spec.rb'
+- './spec/services/packages/go/sync_packages_service_spec.rb'
+- './spec/services/packages/helm/extract_file_metadata_service_spec.rb'
+- './spec/services/packages/helm/process_file_service_spec.rb'
+- './spec/services/packages/mark_package_files_for_destruction_service_spec.rb'
+- './spec/services/packages/mark_package_for_destruction_service_spec.rb'
+- './spec/services/packages/maven/create_package_service_spec.rb'
+- './spec/services/packages/maven/find_or_create_package_service_spec.rb'
+- './spec/services/packages/maven/metadata/append_package_file_service_spec.rb'
+- './spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb'
+- './spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb'
+- './spec/services/packages/maven/metadata/sync_service_spec.rb'
+- './spec/services/packages/npm/create_package_service_spec.rb'
+- './spec/services/packages/npm/create_tag_service_spec.rb'
+- './spec/services/packages/nuget/create_dependency_service_spec.rb'
+- './spec/services/packages/nuget/metadata_extraction_service_spec.rb'
+- './spec/services/packages/nuget/search_service_spec.rb'
+- './spec/services/packages/nuget/sync_metadatum_service_spec.rb'
+- './spec/services/packages/nuget/update_package_from_metadata_service_spec.rb'
+- './spec/services/packages/pypi/create_package_service_spec.rb'
+- './spec/services/packages/remove_tag_service_spec.rb'
+- './spec/services/packages/rubygems/create_dependencies_service_spec.rb'
+- './spec/services/packages/rubygems/create_gemspec_service_spec.rb'
+- './spec/services/packages/rubygems/dependency_resolver_service_spec.rb'
+- './spec/services/packages/rubygems/metadata_extraction_service_spec.rb'
+- './spec/services/packages/rubygems/process_gem_service_spec.rb'
+- './spec/services/packages/terraform_module/create_package_service_spec.rb'
+- './spec/services/packages/update_package_file_service_spec.rb'
+- './spec/services/packages/update_tags_service_spec.rb'
+- './spec/services/pages/delete_service_spec.rb'
+- './spec/services/pages/destroy_deployments_service_spec.rb'
+- './spec/services/pages_domains/create_acme_order_service_spec.rb'
+- './spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb'
+- './spec/services/pages_domains/retry_acme_order_service_spec.rb'
+- './spec/services/pages/migrate_from_legacy_storage_service_spec.rb'
+- './spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb'
+- './spec/services/pages/zip_directory_service_spec.rb'
+- './spec/services/personal_access_tokens/create_service_spec.rb'
+- './spec/services/personal_access_tokens/last_used_service_spec.rb'
+- './spec/services/personal_access_tokens/revoke_service_spec.rb'
+- './spec/services/post_receive_service_spec.rb'
+- './spec/services/preview_markdown_service_spec.rb'
+- './spec/services/product_analytics/build_activity_graph_service_spec.rb'
+- './spec/services/product_analytics/build_graph_service_spec.rb'
+- './spec/services/projects/after_rename_service_spec.rb'
+- './spec/services/projects/alerting/notify_service_spec.rb'
+- './spec/services/projects/all_issues_count_service_spec.rb'
+- './spec/services/projects/all_merge_requests_count_service_spec.rb'
+- './spec/services/projects/android_target_platform_detector_service_spec.rb'
+- './spec/services/projects/apple_target_platform_detector_service_spec.rb'
+- './spec/services/projects/autocomplete_service_spec.rb'
+- './spec/services/projects/auto_devops/disable_service_spec.rb'
+- './spec/services/projects/batch_open_issues_count_service_spec.rb'
+- './spec/services/projects/blame_service_spec.rb'
+- './spec/services/projects/branches_by_mode_service_spec.rb'
+- './spec/services/projects/cleanup_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/destroy_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/count_service_spec.rb'
+- './spec/services/projects/create_from_template_service_spec.rb'
+- './spec/services/projects/create_service_spec.rb'
+- './spec/services/projects/deploy_tokens/create_service_spec.rb'
+- './spec/services/projects/deploy_tokens/destroy_service_spec.rb'
+- './spec/services/projects/destroy_service_spec.rb'
+- './spec/services/projects/detect_repository_languages_service_spec.rb'
+- './spec/services/projects/download_service_spec.rb'
+- './spec/services/projects/enable_deploy_key_service_spec.rb'
+- './spec/services/projects/fetch_statistics_increment_service_spec.rb'
+- './spec/services/projects/forks_count_service_spec.rb'
+- './spec/services/projects/fork_service_spec.rb'
+- './spec/services/projects/git_deduplication_service_spec.rb'
+- './spec/services/projects/gitlab_projects_import_service_spec.rb'
+- './spec/services/projects/group_links/create_service_spec.rb'
+- './spec/services/projects/group_links/destroy_service_spec.rb'
+- './spec/services/projects/group_links/update_service_spec.rb'
+- './spec/services/projects/hashed_storage/base_attachment_service_spec.rb'
+- './spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
+- './spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
+- './spec/services/projects/hashed_storage/migration_service_spec.rb'
+- './spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb'
+- './spec/services/projects/hashed_storage/rollback_repository_service_spec.rb'
+- './spec/services/projects/hashed_storage/rollback_service_spec.rb'
+- './spec/services/projects/import_error_filter_spec.rb'
+- './spec/services/projects/import_export/export_service_spec.rb'
+- './spec/services/projects/import_export/relation_export_service_spec.rb'
+- './spec/services/projects/import_service_spec.rb'
+- './spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb'
+- './spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb'
+- './spec/services/projects/lfs_pointers/lfs_download_service_spec.rb'
+- './spec/services/projects/lfs_pointers/lfs_import_service_spec.rb'
+- './spec/services/projects/lfs_pointers/lfs_link_service_spec.rb'
+- './spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb'
+- './spec/services/projects/move_access_service_spec.rb'
+- './spec/services/projects/move_deploy_keys_projects_service_spec.rb'
+- './spec/services/projects/move_forks_service_spec.rb'
+- './spec/services/projects/move_lfs_objects_projects_service_spec.rb'
+- './spec/services/projects/move_notification_settings_service_spec.rb'
+- './spec/services/projects/move_project_authorizations_service_spec.rb'
+- './spec/services/projects/move_project_group_links_service_spec.rb'
+- './spec/services/projects/move_project_members_service_spec.rb'
+- './spec/services/projects/move_users_star_projects_service_spec.rb'
+- './spec/services/projects/open_issues_count_service_spec.rb'
+- './spec/services/projects/open_merge_requests_count_service_spec.rb'
+- './spec/services/projects/operations/update_service_spec.rb'
+- './spec/services/projects/overwrite_project_service_spec.rb'
+- './spec/services/projects/participants_service_spec.rb'
+- './spec/services/projects/prometheus/alerts/notify_service_spec.rb'
+- './spec/services/projects/prometheus/metrics/destroy_service_spec.rb'
+- './spec/services/projects/protect_default_branch_service_spec.rb'
+- './spec/services/projects/readme_renderer_service_spec.rb'
+- './spec/services/projects/record_target_platforms_service_spec.rb'
+- './spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb'
+- './spec/services/projects/repository_languages_service_spec.rb'
+- './spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb'
+- './spec/services/projects/transfer_service_spec.rb'
+- './spec/services/projects/unlink_fork_service_spec.rb'
+- './spec/services/projects/update_pages_service_spec.rb'
+- './spec/services/projects/update_remote_mirror_service_spec.rb'
+- './spec/services/projects/update_repository_storage_service_spec.rb'
+- './spec/services/projects/update_service_spec.rb'
+- './spec/services/projects/update_statistics_service_spec.rb'
+- './spec/services/prometheus/proxy_service_spec.rb'
+- './spec/services/prometheus/proxy_variable_substitution_service_spec.rb'
+- './spec/services/protected_branches/cache_service_spec.rb'
+- './spec/services/protected_branches/create_service_spec.rb'
+- './spec/services/protected_branches/destroy_service_spec.rb'
+- './spec/services/protected_branches/update_service_spec.rb'
+- './spec/services/protected_tags/create_service_spec.rb'
+- './spec/services/protected_tags/destroy_service_spec.rb'
+- './spec/services/protected_tags/update_service_spec.rb'
+- './spec/services/push_event_payload_service_spec.rb'
+- './spec/services/quick_actions/interpret_service_spec.rb'
+- './spec/services/quick_actions/target_service_spec.rb'
+- './spec/services/releases/create_evidence_service_spec.rb'
+- './spec/services/releases/create_service_spec.rb'
+- './spec/services/releases/destroy_service_spec.rb'
+- './spec/services/releases/update_service_spec.rb'
+- './spec/services/repositories/changelog_service_spec.rb'
+- './spec/services/repositories/destroy_service_spec.rb'
+- './spec/services/repositories/housekeeping_service_spec.rb'
+- './spec/services/repository_archive_clean_up_service_spec.rb'
+- './spec/services/reset_project_cache_service_spec.rb'
+- './spec/services/resource_access_tokens/create_service_spec.rb'
+- './spec/services/resource_access_tokens/revoke_service_spec.rb'
+- './spec/services/resource_events/change_labels_service_spec.rb'
+- './spec/services/resource_events/change_milestone_service_spec.rb'
+- './spec/services/resource_events/change_state_service_spec.rb'
+- './spec/services/resource_events/merge_into_notes_service_spec.rb'
+- './spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb'
+- './spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb'
+- './spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb'
+- './spec/services/search/global_service_spec.rb'
+- './spec/services/search/group_service_spec.rb'
+- './spec/services/search_service_spec.rb'
+- './spec/services/search/snippet_service_spec.rb'
+- './spec/services/security/ci_configuration/container_scanning_create_service_spec.rb'
+- './spec/services/security/ci_configuration/sast_create_service_spec.rb'
+- './spec/services/security/ci_configuration/sast_iac_create_service_spec.rb'
+- './spec/services/security/ci_configuration/sast_parser_service_spec.rb'
+- './spec/services/security/ci_configuration/secret_detection_create_service_spec.rb'
+- './spec/services/security/merge_reports_service_spec.rb'
+- './spec/services/serverless/associate_domain_service_spec.rb'
+- './spec/services/service_desk_settings/update_service_spec.rb'
+- './spec/services/service_ping/submit_service_ping_service_spec.rb'
+- './spec/services/service_response_spec.rb'
+- './spec/services/snippets/bulk_destroy_service_spec.rb'
+- './spec/services/snippets/count_service_spec.rb'
+- './spec/services/snippets/create_service_spec.rb'
+- './spec/services/snippets/destroy_service_spec.rb'
+- './spec/services/snippets/repository_validation_service_spec.rb'
+- './spec/services/snippets/schedule_bulk_repository_shard_moves_service_spec.rb'
+- './spec/services/snippets/update_repository_storage_service_spec.rb'
+- './spec/services/snippets/update_service_spec.rb'
+- './spec/services/snippets/update_statistics_service_spec.rb'
+- './spec/services/spam/akismet_mark_as_spam_service_spec.rb'
+- './spec/services/spam/akismet_service_spec.rb'
+- './spec/services/spam/ham_service_spec.rb'
+- './spec/services/spam/spam_action_service_spec.rb'
+- './spec/services/spam/spam_params_spec.rb'
+- './spec/services/spam/spam_verdict_service_spec.rb'
+- './spec/services/submodules/update_service_spec.rb'
+- './spec/services/suggestions/apply_service_spec.rb'
+- './spec/services/suggestions/create_service_spec.rb'
+- './spec/services/suggestions/outdate_service_spec.rb'
+- './spec/services/system_hooks_service_spec.rb'
+- './spec/services/system_notes/alert_management_service_spec.rb'
+- './spec/services/system_notes/base_service_spec.rb'
+- './spec/services/system_notes/commit_service_spec.rb'
+- './spec/services/system_notes/design_management_service_spec.rb'
+- './spec/services/system_note_service_spec.rb'
+- './spec/services/system_notes/incident_service_spec.rb'
+- './spec/services/system_notes/incidents_service_spec.rb'
+- './spec/services/system_notes/issuables_service_spec.rb'
+- './spec/services/system_notes/merge_requests_service_spec.rb'
+- './spec/services/system_notes/time_tracking_service_spec.rb'
+- './spec/services/system_notes/zoom_service_spec.rb'
+- './spec/services/tags/create_service_spec.rb'
+- './spec/services/tags/destroy_service_spec.rb'
+- './spec/services/task_list_toggle_service_spec.rb'
+- './spec/services/tasks_to_be_done/base_service_spec.rb'
+- './spec/services/terraform/remote_state_handler_spec.rb'
+- './spec/services/terraform/states/destroy_service_spec.rb'
+- './spec/services/terraform/states/trigger_destroy_service_spec.rb'
+- './spec/services/test_hooks/project_service_spec.rb'
+- './spec/services/test_hooks/system_service_spec.rb'
+- './spec/services/timelogs/create_service_spec.rb'
+- './spec/services/timelogs/delete_service_spec.rb'
+- './spec/services/todos/allowed_target_filter_service_spec.rb'
+- './spec/services/todos/destroy/confidential_issue_service_spec.rb'
+- './spec/services/todos/destroy/design_service_spec.rb'
+- './spec/services/todos/destroy/destroyed_issuable_service_spec.rb'
+- './spec/services/todos/destroy/entity_leave_service_spec.rb'
+- './spec/services/todos/destroy/group_private_service_spec.rb'
+- './spec/services/todos/destroy/project_private_service_spec.rb'
+- './spec/services/todos/destroy/unauthorized_features_service_spec.rb'
+- './spec/services/todo_service_spec.rb'
+- './spec/services/topics/merge_service_spec.rb'
+- './spec/services/two_factor/destroy_service_spec.rb'
+- './spec/services/update_container_registry_info_service_spec.rb'
+- './spec/services/update_merge_request_metrics_service_spec.rb'
+- './spec/services/uploads/destroy_service_spec.rb'
+- './spec/services/upload_service_spec.rb'
+- './spec/services/user_preferences/update_service_spec.rb'
+- './spec/services/user_project_access_changed_service_spec.rb'
+- './spec/services/users/activity_service_spec.rb'
+- './spec/services/users/approve_service_spec.rb'
+- './spec/services/users/authorized_build_service_spec.rb'
+- './spec/services/users/banned_user_base_service_spec.rb'
+- './spec/services/users/ban_service_spec.rb'
+- './spec/services/users/batch_status_cleaner_service_spec.rb'
+- './spec/services/users/block_service_spec.rb'
+- './spec/services/users/build_service_spec.rb'
+- './spec/services/users/create_service_spec.rb'
+- './spec/services/users/destroy_service_spec.rb'
+- './spec/services/users/dismiss_callout_service_spec.rb'
+- './spec/services/users/dismiss_group_callout_service_spec.rb'
+- './spec/services/users/dismiss_namespace_callout_service_spec.rb'
+- './spec/services/users/dismiss_project_callout_service_spec.rb'
+- './spec/services/users/email_verification/generate_token_service_spec.rb'
+- './spec/services/users/email_verification/validate_token_service_spec.rb'
+- './spec/services/users/in_product_marketing_email_records_spec.rb'
+- './spec/services/users/keys_count_service_spec.rb'
+- './spec/services/users/last_push_event_service_spec.rb'
+- './spec/services/users/migrate_to_ghost_user_service_spec.rb'
+- './spec/services/users/refresh_authorized_projects_service_spec.rb'
+- './spec/services/users/registrations_build_service_spec.rb'
+- './spec/services/users/reject_service_spec.rb'
+- './spec/services/users/repair_ldap_blocked_service_spec.rb'
+- './spec/services/users/respond_to_terms_service_spec.rb'
+- './spec/services/users/saved_replies/create_service_spec.rb'
+- './spec/services/users/saved_replies/destroy_service_spec.rb'
+- './spec/services/users/saved_replies/update_service_spec.rb'
+- './spec/services/users/set_status_service_spec.rb'
+- './spec/services/users/signup_service_spec.rb'
+- './spec/services/users/unban_service_spec.rb'
+- './spec/services/users/update_canonical_email_service_spec.rb'
+- './spec/services/users/update_highest_member_role_service_spec.rb'
+- './spec/services/users/update_service_spec.rb'
+- './spec/services/users/update_todo_count_cache_service_spec.rb'
+- './spec/services/users/upsert_credit_card_validation_service_spec.rb'
+- './spec/services/users/validate_manual_otp_service_spec.rb'
+- './spec/services/users/validate_push_otp_service_spec.rb'
+- './spec/services/verify_pages_domain_service_spec.rb'
+- './spec/services/webauthn/authenticate_service_spec.rb'
+- './spec/services/webauthn/register_service_spec.rb'
+- './spec/services/web_hooks/destroy_service_spec.rb'
+- './spec/services/web_hook_service_spec.rb'
+- './spec/services/web_hooks/log_destroy_service_spec.rb'
+- './spec/services/web_hooks/log_execution_service_spec.rb'
+- './spec/services/wiki_pages/base_service_spec.rb'
+- './spec/services/wiki_pages/create_service_spec.rb'
+- './spec/services/wiki_pages/destroy_service_spec.rb'
+- './spec/services/wiki_pages/event_create_service_spec.rb'
+- './spec/services/wiki_pages/update_service_spec.rb'
+- './spec/services/wikis/create_attachment_service_spec.rb'
+- './spec/services/work_items/build_service_spec.rb'
+- './spec/services/work_items/create_and_link_service_spec.rb'
+- './spec/services/work_items/create_from_task_service_spec.rb'
+- './spec/services/work_items/create_service_spec.rb'
+- './spec/services/work_items/delete_service_spec.rb'
+- './spec/services/work_items/delete_task_service_spec.rb'
+- './spec/services/work_items/parent_links/create_service_spec.rb'
+- './spec/services/work_items/parent_links/destroy_service_spec.rb'
+- './spec/services/work_items/task_list_reference_removal_service_spec.rb'
+- './spec/services/work_items/task_list_reference_replacement_service_spec.rb'
+- './spec/services/work_items/update_service_spec.rb'
+- './spec/services/work_items/widgets/assignees_service/update_service_spec.rb'
+- './spec/services/work_items/widgets/description_service/update_service_spec.rb'
+- './spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb'
+- './spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb'
+- './spec/services/x509_certificate_revoke_service_spec.rb'
+- './spec/sidekiq_cluster/sidekiq_cluster_spec.rb'
+- './spec/sidekiq/cron/job_gem_dependency_spec.rb'
+- './spec/spam/concerns/has_spam_action_response_fields_spec.rb'
+- './spec/support_specs/database/multiple_databases_spec.rb'
+- './spec/support_specs/database/prevent_cross_joins_spec.rb'
+- './spec/support_specs/graphql/arguments_spec.rb'
+- './spec/support_specs/graphql/field_selection_spec.rb'
+- './spec/support_specs/graphql/var_spec.rb'
+- './spec/support_specs/helpers/active_record/query_recorder_spec.rb'
+- './spec/support_specs/helpers/graphql_helpers_spec.rb'
+- './spec/support_specs/helpers/migrations_helpers_spec.rb'
+- './spec/support_specs/helpers/redis_commands/recorder_spec.rb'
+- './spec/support_specs/helpers/stub_feature_flags_spec.rb'
+- './spec/support_specs/helpers/stub_method_calls_spec.rb'
+- './spec/support_specs/matchers/be_sorted_spec.rb'
+- './spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
+- './spec/support_specs/time_travel_spec.rb'
+- './spec/tasks/admin_mode_spec.rb'
+- './spec/tasks/cache/clear/redis_spec.rb'
+- './spec/tasks/config_lint_spec.rb'
+- './spec/tasks/dev_rake_spec.rb'
+- './spec/tasks/gettext_rake_spec.rb'
+- './spec/tasks/gitlab/artifacts/check_rake_spec.rb'
+- './spec/tasks/gitlab/artifacts/migrate_rake_spec.rb'
+- './spec/tasks/gitlab/background_migrations_rake_spec.rb'
+- './spec/tasks/gitlab/backup_rake_spec.rb'
+- './spec/tasks/gitlab/check_rake_spec.rb'
+- './spec/tasks/gitlab/cleanup_rake_spec.rb'
+- './spec/tasks/gitlab/container_registry_rake_spec.rb'
+- './spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb'
+- './spec/tasks/gitlab/db/lock_writes_rake_spec.rb'
+- './spec/tasks/gitlab/db_rake_spec.rb'
+- './spec/tasks/gitlab/db/validate_config_rake_spec.rb'
+- './spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb'
+- './spec/tasks/gitlab/external_diffs_rake_spec.rb'
+- './spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb'
+- './spec/tasks/gitlab/gitaly_rake_spec.rb'
+- './spec/tasks/gitlab/git_rake_spec.rb'
+- './spec/tasks/gitlab/ldap_rake_spec.rb'
+- './spec/tasks/gitlab/lfs/check_rake_spec.rb'
+- './spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
+- './spec/tasks/gitlab/packages/events_rake_spec.rb'
+- './spec/tasks/gitlab/packages/migrate_rake_spec.rb'
+- './spec/tasks/gitlab/pages_rake_spec.rb'
+- './spec/tasks/gitlab/password_rake_spec.rb'
+- './spec/tasks/gitlab/praefect_rake_spec.rb'
+- './spec/tasks/gitlab/refresh_project_statistics_build_artifacts_size_rake_spec.rb'
+- './spec/tasks/gitlab/seed/group_seed_rake_spec.rb'
+- './spec/tasks/gitlab/setup_rake_spec.rb'
+- './spec/tasks/gitlab/shell_rake_spec.rb'
+- './spec/tasks/gitlab/sidekiq_rake_spec.rb'
+- './spec/tasks/gitlab/smtp_rake_spec.rb'
+- './spec/tasks/gitlab/snippets_rake_spec.rb'
+- './spec/tasks/gitlab/storage_rake_spec.rb'
+- './spec/tasks/gitlab/task_helpers_spec.rb'
+- './spec/tasks/gitlab/terraform/migrate_rake_spec.rb'
+- './spec/tasks/gitlab/update_templates_rake_spec.rb'
+- './spec/tasks/gitlab/uploads/check_rake_spec.rb'
+- './spec/tasks/gitlab/uploads/migrate_rake_spec.rb'
+- './spec/tasks/gitlab/usage_data_rake_spec.rb'
+- './spec/tasks/gitlab/user_management_rake_spec.rb'
+- './spec/tasks/gitlab/web_hook_rake_spec.rb'
+- './spec/tasks/gitlab/workhorse_rake_spec.rb'
+- './spec/tasks/gitlab/x509/update_rake_spec.rb'
+- './spec/tasks/migrate/schema_check_rake_spec.rb'
+- './spec/tasks/rubocop_rake_spec.rb'
+- './spec/tasks/tokens_spec.rb'
+- './spec/tooling/danger/customer_success_spec.rb'
+- './spec/tooling/danger/datateam_spec.rb'
+- './spec/tooling/danger/feature_flag_spec.rb'
+- './spec/tooling/danger/product_intelligence_spec.rb'
+- './spec/tooling/danger/project_helper_spec.rb'
+- './spec/tooling/danger/sidekiq_queues_spec.rb'
+- './spec/tooling/danger/specs_spec.rb'
+- './spec/tooling/docs/deprecation_handling_spec.rb'
+- './spec/tooling/graphql/docs/renderer_spec.rb'
+- './spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb'
+- './spec/tooling/lib/tooling/crystalball/coverage_lines_strategy_spec.rb'
+- './spec/tooling/lib/tooling/find_codeowners_spec.rb'
+- './spec/tooling/lib/tooling/helm3_client_spec.rb'
+- './spec/tooling/lib/tooling/kubernetes_client_spec.rb'
+- './spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb'
+- './spec/tooling/lib/tooling/test_map_generator_spec.rb'
+- './spec/tooling/lib/tooling/test_map_packer_spec.rb'
+- './spec/tooling/merge_request_spec.rb'
+- './spec/tooling/quality/test_level_spec.rb'
+- './spec/tooling/rspec_flaky/config_spec.rb'
+- './spec/tooling/rspec_flaky/example_spec.rb'
+- './spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb'
+- './spec/tooling/rspec_flaky/flaky_example_spec.rb'
+- './spec/tooling/rspec_flaky/listener_spec.rb'
+- './spec/tooling/rspec_flaky/report_spec.rb'
+- './spec/uploaders/attachment_uploader_spec.rb'
+- './spec/uploaders/avatar_uploader_spec.rb'
+- './spec/uploaders/ci/pipeline_artifact_uploader_spec.rb'
+- './spec/uploaders/ci/secure_file_uploader_spec.rb'
+- './spec/uploaders/content_type_whitelist_spec.rb'
+- './spec/uploaders/dependency_proxy/file_uploader_spec.rb'
+- './spec/uploaders/design_management/design_v432x230_uploader_spec.rb'
+- './spec/uploaders/external_diff_uploader_spec.rb'
+- './spec/uploaders/favicon_uploader_spec.rb'
+- './spec/uploaders/file_mover_spec.rb'
+- './spec/uploaders/file_uploader_spec.rb'
+- './spec/uploaders/gitlab_uploader_spec.rb'
+- './spec/uploaders/import_export_uploader_spec.rb'
+- './spec/uploaders/job_artifact_uploader_spec.rb'
+- './spec/uploaders/lfs_object_uploader_spec.rb'
+- './spec/uploaders/metric_image_uploader_spec.rb'
+- './spec/uploaders/namespace_file_uploader_spec.rb'
+- './spec/uploaders/object_storage_spec.rb'
+- './spec/uploaders/packages/composer/cache_uploader_spec.rb'
+- './spec/uploaders/packages/debian/component_file_uploader_spec.rb'
+- './spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb'
+- './spec/uploaders/packages/package_file_uploader_spec.rb'
+- './spec/uploaders/pages/deployment_uploader_spec.rb'
+- './spec/uploaders/personal_file_uploader_spec.rb'
+- './spec/uploaders/records_uploads_spec.rb'
+- './spec/uploaders/terraform/state_uploader_spec.rb'
+- './spec/uploaders/uploader_helper_spec.rb'
+- './spec/uploaders/workers/object_storage/background_move_worker_spec.rb'
+- './spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb'
+- './spec/validators/addressable_url_validator_spec.rb'
+- './spec/validators/any_field_validator_spec.rb'
+- './spec/validators/array_members_validator_spec.rb'
+- './spec/validators/branch_filter_validator_spec.rb'
+- './spec/validators/color_validator_spec.rb'
+- './spec/validators/cron_freeze_period_timezone_validator_spec.rb'
+- './spec/validators/cron_validator_spec.rb'
+- './spec/validators/devise_email_validator_spec.rb'
+- './spec/validators/future_date_validator_spec.rb'
+- './spec/validators/gitlab/zoom_url_validator_spec.rb'
+- './spec/validators/html_safety_validator_spec.rb'
+- './spec/validators/import/gitlab_projects/remote_file_validator_spec.rb'
+- './spec/validators/ip_address_validator_spec.rb'
+- './spec/validators/json_schema_validator_spec.rb'
+- './spec/validators/js_regex_validator_spec.rb'
+- './spec/validators/named_ecdsa_key_validator_spec.rb'
+- './spec/validators/namespace_path_validator_spec.rb'
+- './spec/validators/nested_attributes_duplicates_validator_spec.rb'
+- './spec/validators/project_path_validator_spec.rb'
+- './spec/validators/public_url_validator_spec.rb'
+- './spec/validators/qualified_domain_array_validator_spec.rb'
+- './spec/validators/rsa_key_validator_spec.rb'
+- './spec/validators/sha_validator_spec.rb'
+- './spec/validators/system_hook_url_validator_spec.rb'
+- './spec/validators/x509_certificate_credentials_validator_spec.rb'
+- './spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb'
+- './spec/views/admin/application_settings/ci_cd.html.haml_spec.rb'
+- './spec/views/admin/application_settings/_eks.html.haml_spec.rb'
+- './spec/views/admin/application_settings/general.html.haml_spec.rb'
+- './spec/views/admin/application_settings/_package_registry.html.haml_spec.rb'
+- './spec/views/admin/application_settings/_repository_check.html.haml_spec.rb'
+- './spec/views/admin/application_settings/repository.html.haml_spec.rb'
+- './spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb'
+- './spec/views/admin/broadcast_messages/index.html.haml_spec.rb'
+- './spec/views/admin/dashboard/index.html.haml_spec.rb'
+- './spec/views/admin/identities/index.html.haml_spec.rb'
+- './spec/views/admin/sessions/new.html.haml_spec.rb'
+- './spec/views/admin/sessions/two_factor.html.haml_spec.rb'
+- './spec/views/ci/status/_badge.html.haml_spec.rb'
+- './spec/views/ci/status/_icon.html.haml_spec.rb'
+- './spec/views/dashboard/milestones/index.html.haml_spec.rb'
+- './spec/views/dashboard/projects/_blank_state_admin_welcome.haml_spec.rb'
+- './spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb'
+- './spec/views/dashboard/projects/index.html.haml_spec.rb'
+- './spec/views/dashboard/projects/_nav.html.haml_spec.rb'
+- './spec/views/devise/sessions/new.html.haml_spec.rb'
+- './spec/views/devise/shared/_signin_box.html.haml_spec.rb'
+- './spec/views/devise/shared/_signup_box.html.haml_spec.rb'
+- './spec/views/errors/access_denied.html.haml_spec.rb'
+- './spec/views/errors/omniauth_error.html.haml_spec.rb'
+- './spec/views/events/event/_push.html.haml_spec.rb'
+- './spec/views/groups/edit.html.haml_spec.rb'
+- './spec/views/groups/group_members/index.html.haml_spec.rb'
+- './spec/views/groups/_home_panel.html.haml_spec.rb'
+- './spec/views/groups/milestones/index.html.haml_spec.rb'
+- './spec/views/groups/new.html.haml_spec.rb'
+- './spec/views/groups/settings/_remove.html.haml_spec.rb'
+- './spec/views/help/index.html.haml_spec.rb'
+- './spec/views/help/instance_configuration.html.haml_spec.rb'
+- './spec/views/help/show.html.haml_spec.rb'
+- './spec/views/import/gitlab_projects/new.html.haml_spec.rb'
+- './spec/views/layouts/application.html.haml_spec.rb'
+- './spec/views/layouts/devise_empty.html.haml_spec.rb'
+- './spec/views/layouts/devise.html.haml_spec.rb'
+- './spec/views/layouts/_flash.html.haml_spec.rb'
+- './spec/views/layouts/fullscreen.html.haml_spec.rb'
+- './spec/views/layouts/header/_gitlab_version.html.haml_spec.rb'
+- './spec/views/layouts/header/_new_dropdown.haml_spec.rb'
+- './spec/views/layouts/_header_search.html.haml_spec.rb'
+- './spec/views/layouts/_head.html.haml_spec.rb'
+- './spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb'
+- './spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb'
+- './spec/views/layouts/nav/sidebar/_profile.html.haml_spec.rb'
+- './spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb'
+- './spec/views/layouts/profile.html.haml_spec.rb'
+- './spec/views/layouts/_published_experiments.html.haml_spec.rb'
+- './spec/views/layouts/_search.html.haml_spec.rb'
+- './spec/views/layouts/signup_onboarding.html.haml_spec.rb'
+- './spec/views/layouts/simple_registration.html.haml_spec.rb'
+- './spec/views/layouts/terms.html.haml_spec.rb'
+- './spec/views/notify/autodevops_disabled_email.text.erb_spec.rb'
+- './spec/views/notify/changed_milestone_email.html.haml_spec.rb'
+- './spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb'
+- './spec/views/notify/change_in_merge_request_draft_status_email.text.erb_spec.rb'
+- './spec/views/notify/pipeline_failed_email.html.haml_spec.rb'
+- './spec/views/notify/pipeline_failed_email.text.erb_spec.rb'
+- './spec/views/notify/pipeline_fixed_email.html.haml_spec.rb'
+- './spec/views/notify/pipeline_fixed_email.text.erb_spec.rb'
+- './spec/views/notify/pipeline_success_email.html.haml_spec.rb'
+- './spec/views/notify/pipeline_success_email.text.erb_spec.rb'
+- './spec/views/notify/push_to_merge_request_email.text.haml_spec.rb'
+- './spec/views/profiles/audit_log.html.haml_spec.rb'
+- './spec/views/profiles/keys/_form.html.haml_spec.rb'
+- './spec/views/profiles/keys/_key.html.haml_spec.rb'
+- './spec/views/profiles/notifications/show.html.haml_spec.rb'
+- './spec/views/profiles/preferences/show.html.haml_spec.rb'
+- './spec/views/profiles/show.html.haml_spec.rb'
+- './spec/views/projects/artifacts/_artifact.html.haml_spec.rb'
+- './spec/views/projects/blob/_viewer.html.haml_spec.rb'
+- './spec/views/projects/branches/index.html.haml_spec.rb'
+- './spec/views/projects/commit/branches.html.haml_spec.rb'
+- './spec/views/projects/commit/_commit_box.html.haml_spec.rb'
+- './spec/views/projects/commits/_commit.html.haml_spec.rb'
+- './spec/views/projects/commit/show.html.haml_spec.rb'
+- './spec/views/projects/commits/show.html.haml_spec.rb'
+- './spec/views/projects/diffs/_viewer.html.haml_spec.rb'
+- './spec/views/projects/edit.html.haml_spec.rb'
+- './spec/views/projects/empty.html.haml_spec.rb'
+- './spec/views/projects/environments/terminal.html.haml_spec.rb'
+- './spec/views/projects/_flash_messages.html.haml_spec.rb'
+- './spec/views/projects/_home_panel.html.haml_spec.rb'
+- './spec/views/projects/hooks/edit.html.haml_spec.rb'
+- './spec/views/projects/hooks/index.html.haml_spec.rb'
+- './spec/views/projects/imports/new.html.haml_spec.rb'
+- './spec/views/projects/issues/_issue.html.haml_spec.rb'
+- './spec/views/projects/issues/_related_branches.html.haml_spec.rb'
+- './spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb'
+- './spec/views/projects/issues/show.html.haml_spec.rb'
+- './spec/views/projects/jobs/_build.html.haml_spec.rb'
+- './spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb'
+- './spec/views/projects/jobs/show.html.haml_spec.rb'
+- './spec/views/projects/merge_requests/_commits.html.haml_spec.rb'
+- './spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb'
+- './spec/views/projects/merge_requests/edit.html.haml_spec.rb'
+- './spec/views/projects/merge_requests/show.html.haml_spec.rb'
+- './spec/views/projects/milestones/index.html.haml_spec.rb'
+- './spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb'
+- './spec/views/projects/pages_domains/show.html.haml_spec.rb'
+- './spec/views/projects/pages/new.html.haml_spec.rb'
+- './spec/views/projects/pages/show.html.haml_spec.rb'
+- './spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
+- './spec/views/projects/pipelines/show.html.haml_spec.rb'
+- './spec/views/projects/project_members/index.html.haml_spec.rb'
+- './spec/views/projects/runners/_specific_runners.html.haml_spec.rb'
+- './spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb'
+- './spec/views/projects/settings/integrations/edit.html.haml_spec.rb'
+- './spec/views/projects/settings/operations/show.html.haml_spec.rb'
+- './spec/views/projects/tags/index.html.haml_spec.rb'
+- './spec/views/projects/tree/show.html.haml_spec.rb'
+- './spec/views/registrations/welcome/show.html.haml_spec.rb'
+- './spec/views/search/_results.html.haml_spec.rb'
+- './spec/views/search/show.html.haml_spec.rb'
+- './spec/views/shared/access_tokens/_table.html.haml_spec.rb'
+- './spec/views/shared/deploy_tokens/_form.html.haml_spec.rb'
+- './spec/views/shared/groups/_dropdown.html.haml_spec.rb'
+- './spec/views/shared/issuable/_sidebar.html.haml_spec.rb'
+- './spec/views/shared/_label_row.html.haml_spec.rb'
+- './spec/views/shared/milestones/_issuable.html.haml_spec.rb'
+- './spec/views/shared/milestones/_issuables.html.haml_spec.rb'
+- './spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb'
+- './spec/views/shared/milestones/_top.html.haml_spec.rb'
+- './spec/views/shared/nav/_sidebar.html.haml_spec.rb'
+- './spec/views/shared/notes/_form.html.haml_spec.rb'
+- './spec/views/shared/projects/_inactive_project_deletion_alert.html.haml_spec.rb'
+- './spec/views/shared/projects/_list.html.haml_spec.rb'
+- './spec/views/shared/projects/_project.html.haml_spec.rb'
+- './spec/views/shared/runners/_runner_details.html.haml_spec.rb'
+- './spec/views/shared/snippets/_snippet.html.haml_spec.rb'
+- './spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb'
+- './spec/views/shared/wikis/_sidebar.html.haml_spec.rb'
+- './spec/workers/admin_email_worker_spec.rb'
+- './spec/workers/analytics/usage_trends/counter_job_worker_spec.rb'
+- './spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb'
+- './spec/workers/approve_blocked_pending_approval_users_worker_spec.rb'
+- './spec/workers/authorized_keys_worker_spec.rb'
+- './spec/workers/authorized_projects_worker_spec.rb'
+- './spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb'
+- './spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb'
+- './spec/workers/authorized_project_update/project_recalculate_worker_spec.rb'
+- './spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb'
+- './spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb'
+- './spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb'
+- './spec/workers/auto_devops/disable_worker_spec.rb'
+- './spec/workers/auto_merge_process_worker_spec.rb'
+- './spec/workers/background_migration/ci_database_worker_spec.rb'
+- './spec/workers/background_migration_worker_spec.rb'
+- './spec/workers/build_hooks_worker_spec.rb'
+- './spec/workers/build_queue_worker_spec.rb'
+- './spec/workers/build_success_worker_spec.rb'
+- './spec/workers/bulk_imports/entity_worker_spec.rb'
+- './spec/workers/bulk_imports/export_request_worker_spec.rb'
+- './spec/workers/bulk_imports/pipeline_worker_spec.rb'
+- './spec/workers/bulk_imports/relation_export_worker_spec.rb'
+- './spec/workers/bulk_imports/stuck_import_worker_spec.rb'
+- './spec/workers/bulk_import_worker_spec.rb'
+- './spec/workers/chat_notification_worker_spec.rb'
+- './spec/workers/ci/archive_traces_cron_worker_spec.rb'
+- './spec/workers/ci/archive_trace_worker_spec.rb'
+- './spec/workers/ci/build_finished_worker_spec.rb'
+- './spec/workers/ci/build_prepare_worker_spec.rb'
+- './spec/workers/ci/build_schedule_worker_spec.rb'
+- './spec/workers/ci/build_trace_chunk_flush_worker_spec.rb'
+- './spec/workers/ci/cancel_pipeline_worker_spec.rb'
+- './spec/workers/ci/create_cross_project_pipeline_worker_spec.rb'
+- './spec/workers/ci/create_downstream_pipeline_worker_spec.rb'
+- './spec/workers/ci/daily_build_group_report_results_worker_spec.rb'
+- './spec/workers/ci/delete_objects_worker_spec.rb'
+- './spec/workers/ci/delete_unit_tests_worker_spec.rb'
+- './spec/workers/ci/drop_pipeline_worker_spec.rb'
+- './spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb'
+- './spec/workers/ci/initial_pipeline_process_worker_spec.rb'
+- './spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb'
+- './spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb'
+- './spec/workers/ci/pending_builds/update_group_worker_spec.rb'
+- './spec/workers/ci/pending_builds/update_project_worker_spec.rb'
+- './spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb'
+- './spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb'
+- './spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb'
+- './spec/workers/ci/pipeline_bridge_status_worker_spec.rb'
+- './spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb'
+- './spec/workers/ci_platform_metrics_update_cron_worker_spec.rb'
+- './spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb'
+- './spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb'
+- './spec/workers/ci/retry_pipeline_worker_spec.rb'
+- './spec/workers/ci/runners/process_runner_version_update_worker_spec.rb'
+- './spec/workers/ci/runners/reconcile_existing_runner_versions_cron_worker_spec.rb'
+- './spec/workers/ci/schedule_delete_objects_cron_worker_spec.rb'
+- './spec/workers/ci/stuck_builds/drop_running_worker_spec.rb'
+- './spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb'
+- './spec/workers/ci/test_failure_history_worker_spec.rb'
+- './spec/workers/ci/track_failed_build_worker_spec.rb'
+- './spec/workers/ci/update_locked_unknown_artifacts_worker_spec.rb'
+- './spec/workers/cleanup_container_repository_worker_spec.rb'
+- './spec/workers/cluster_configure_istio_worker_spec.rb'
+- './spec/workers/cluster_provision_worker_spec.rb'
+- './spec/workers/clusters/agents/delete_expired_events_worker_spec.rb'
+- './spec/workers/clusters/applications/activate_integration_worker_spec.rb'
+- './spec/workers/clusters/applications/deactivate_integration_worker_spec.rb'
+- './spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb'
+- './spec/workers/clusters/cleanup/project_namespace_worker_spec.rb'
+- './spec/workers/clusters/cleanup/service_account_worker_spec.rb'
+- './spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb'
+- './spec/workers/cluster_update_app_worker_spec.rb'
+- './spec/workers/cluster_wait_for_app_update_worker_spec.rb'
+- './spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb'
+- './spec/workers/concerns/application_worker_spec.rb'
+- './spec/workers/concerns/cluster_agent_queue_spec.rb'
+- './spec/workers/concerns/cronjob_queue_spec.rb'
+- './spec/workers/concerns/gitlab/github_import/object_importer_spec.rb'
+- './spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb'
+- './spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb'
+- './spec/workers/concerns/gitlab/notify_upon_death_spec.rb'
+- './spec/workers/concerns/limited_capacity/job_tracker_spec.rb'
+- './spec/workers/concerns/limited_capacity/worker_spec.rb'
+- './spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb'
+- './spec/workers/concerns/project_import_options_spec.rb'
+- './spec/workers/concerns/reenqueuer_spec.rb'
+- './spec/workers/concerns/repository_check_queue_spec.rb'
+- './spec/workers/concerns/waitable_worker_spec.rb'
+- './spec/workers/concerns/worker_attributes_spec.rb'
+- './spec/workers/concerns/worker_context_spec.rb'
+- './spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb'
+- './spec/workers/container_expiration_policy_worker_spec.rb'
+- './spec/workers/container_registry/migration/enqueuer_worker_spec.rb'
+- './spec/workers/container_registry/migration/guard_worker_spec.rb'
+- './spec/workers/container_registry/migration/observer_worker_spec.rb'
+- './spec/workers/create_commit_signature_worker_spec.rb'
+- './spec/workers/create_note_diff_file_worker_spec.rb'
+- './spec/workers/create_pipeline_worker_spec.rb'
+- './spec/workers/database/batched_background_migration/ci_database_worker_spec.rb'
+- './spec/workers/database/batched_background_migration_worker_spec.rb'
+- './spec/workers/database/ci_namespace_mirrors_consistency_check_worker_spec.rb'
+- './spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb'
+- './spec/workers/database/drop_detached_partitions_worker_spec.rb'
+- './spec/workers/database/partition_management_worker_spec.rb'
+- './spec/workers/delete_container_repository_worker_spec.rb'
+- './spec/workers/delete_diff_files_worker_spec.rb'
+- './spec/workers/delete_merged_branches_worker_spec.rb'
+- './spec/workers/delete_user_worker_spec.rb'
+- './spec/workers/dependency_proxy/cleanup_blob_worker_spec.rb'
+- './spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb'
+- './spec/workers/dependency_proxy/cleanup_manifest_worker_spec.rb'
+- './spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb'
+- './spec/workers/deployments/archive_in_project_worker_spec.rb'
+- './spec/workers/deployments/drop_older_deployments_worker_spec.rb'
+- './spec/workers/deployments/hooks_worker_spec.rb'
+- './spec/workers/deployments/link_merge_request_worker_spec.rb'
+- './spec/workers/deployments/update_environment_worker_spec.rb'
+- './spec/workers/design_management/copy_design_collection_worker_spec.rb'
+- './spec/workers/design_management/new_version_worker_spec.rb'
+- './spec/workers/destroy_pages_deployments_worker_spec.rb'
+- './spec/workers/detect_repository_languages_worker_spec.rb'
+- './spec/workers/disallow_two_factor_for_group_worker_spec.rb'
+- './spec/workers/disallow_two_factor_for_subgroups_worker_spec.rb'
+- './spec/workers/email_receiver_worker_spec.rb'
+- './spec/workers/emails_on_push_worker_spec.rb'
+- './spec/workers/environments/auto_delete_cron_worker_spec.rb'
+- './spec/workers/environments/auto_stop_cron_worker_spec.rb'
+- './spec/workers/environments/auto_stop_worker_spec.rb'
+- './spec/workers/environments/canary_ingress/update_worker_spec.rb'
+- './spec/workers/error_tracking_issue_link_worker_spec.rb'
+- './spec/workers/every_sidekiq_worker_spec.rb'
+- './spec/workers/experiments/record_conversion_event_worker_spec.rb'
+- './spec/workers/expire_build_artifacts_worker_spec.rb'
+- './spec/workers/export_csv_worker_spec.rb'
+- './spec/workers/external_service_reactive_caching_worker_spec.rb'
+- './spec/workers/file_hook_worker_spec.rb'
+- './spec/workers/flush_counter_increments_worker_spec.rb'
+- './spec/workers/gitlab/github_import/advance_stage_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_issue_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_note_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb'
+- './spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb'
+- './spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb'
+- './spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb'
+- './spec/workers/gitlab/import/stuck_import_job_spec.rb'
+- './spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/import_issue_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb'
+- './spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb'
+- './spec/workers/gitlab_performance_bar_stats_worker_spec.rb'
+- './spec/workers/gitlab/phabricator_import/base_worker_spec.rb'
+- './spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb'
+- './spec/workers/gitlab_service_ping_worker_spec.rb'
+- './spec/workers/gitlab_shell_worker_spec.rb'
+- './spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb'
+- './spec/workers/group_destroy_worker_spec.rb'
+- './spec/workers/group_export_worker_spec.rb'
+- './spec/workers/group_import_worker_spec.rb'
+- './spec/workers/groups/update_statistics_worker_spec.rb'
+- './spec/workers/hashed_storage/migrator_worker_spec.rb'
+- './spec/workers/hashed_storage/project_migrate_worker_spec.rb'
+- './spec/workers/hashed_storage/project_rollback_worker_spec.rb'
+- './spec/workers/hashed_storage/rollbacker_worker_spec.rb'
+- './spec/workers/import_issues_csv_worker_spec.rb'
+- './spec/workers/incident_management/add_severity_system_note_worker_spec.rb'
+- './spec/workers/incident_management/close_incident_worker_spec.rb'
+- './spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb'
+- './spec/workers/incident_management/process_alert_worker_v2_spec.rb'
+- './spec/workers/integrations/create_external_cross_reference_worker_spec.rb'
+- './spec/workers/integrations/execute_worker_spec.rb'
+- './spec/workers/integrations/irker_worker_spec.rb'
+- './spec/workers/invalid_gpg_signature_update_worker_spec.rb'
+- './spec/workers/issuable_export_csv_worker_spec.rb'
+- './spec/workers/issuable/label_links_destroy_worker_spec.rb'
+- './spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb'
+- './spec/workers/issue_due_scheduler_worker_spec.rb'
+- './spec/workers/issues/placement_worker_spec.rb'
+- './spec/workers/issues/rebalancing_worker_spec.rb'
+- './spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb'
+- './spec/workers/jira_connect/forward_event_worker_spec.rb'
+- './spec/workers/jira_connect/retry_request_worker_spec.rb'
+- './spec/workers/jira_connect/sync_branch_worker_spec.rb'
+- './spec/workers/jira_connect/sync_builds_worker_spec.rb'
+- './spec/workers/jira_connect/sync_deployments_worker_spec.rb'
+- './spec/workers/jira_connect/sync_feature_flags_worker_spec.rb'
+- './spec/workers/jira_connect/sync_merge_request_worker_spec.rb'
+- './spec/workers/jira_connect/sync_project_worker_spec.rb'
+- './spec/workers/loose_foreign_keys/cleanup_worker_spec.rb'
+- './spec/workers/mail_scheduler/issue_due_worker_spec.rb'
+- './spec/workers/mail_scheduler/notification_service_worker_spec.rb'
+- './spec/workers/member_invitation_reminder_emails_worker_spec.rb'
+- './spec/workers/members_destroyer/unassign_issuables_worker_spec.rb'
+- './spec/workers/merge_request_cleanup_refs_worker_spec.rb'
+- './spec/workers/merge_request_mergeability_check_worker_spec.rb'
+- './spec/workers/merge_requests/close_issue_worker_spec.rb'
+- './spec/workers/merge_requests/create_approval_event_worker_spec.rb'
+- './spec/workers/merge_requests/create_approval_note_worker_spec.rb'
+- './spec/workers/merge_requests/create_pipeline_worker_spec.rb'
+- './spec/workers/merge_requests/delete_source_branch_worker_spec.rb'
+- './spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb'
+- './spec/workers/merge_requests/handle_assignees_change_worker_spec.rb'
+- './spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb'
+- './spec/workers/merge_requests/resolve_todos_worker_spec.rb'
+- './spec/workers/merge_requests/update_head_pipeline_worker_spec.rb'
+- './spec/workers/merge_worker_spec.rb'
+- './spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb'
+- './spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb'
+- './spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb'
+- './spec/workers/migrate_external_diffs_worker_spec.rb'
+- './spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb'
+- './spec/workers/namespaces/onboarding_issue_created_worker_spec.rb'
+- './spec/workers/namespaces/onboarding_pipeline_created_worker_spec.rb'
+- './spec/workers/namespaces/onboarding_progress_worker_spec.rb'
+- './spec/workers/namespaces/onboarding_user_added_worker_spec.rb'
+- './spec/workers/namespaces/process_sync_events_worker_spec.rb'
+- './spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb'
+- './spec/workers/namespaces/root_statistics_worker_spec.rb'
+- './spec/workers/namespaces/schedule_aggregation_worker_spec.rb'
+- './spec/workers/namespaces/update_root_statistics_worker_spec.rb'
+- './spec/workers/new_issue_worker_spec.rb'
+- './spec/workers/new_merge_request_worker_spec.rb'
+- './spec/workers/new_note_worker_spec.rb'
+- './spec/workers/object_pool/create_worker_spec.rb'
+- './spec/workers/object_pool/destroy_worker_spec.rb'
+- './spec/workers/object_pool/join_worker_spec.rb'
+- './spec/workers/packages/cleanup/execute_policy_worker_spec.rb'
+- './spec/workers/packages/cleanup_package_file_worker_spec.rb'
+- './spec/workers/packages/cleanup_package_registry_worker_spec.rb'
+- './spec/workers/packages/composer/cache_cleanup_worker_spec.rb'
+- './spec/workers/packages/composer/cache_update_worker_spec.rb'
+- './spec/workers/packages/debian/generate_distribution_worker_spec.rb'
+- './spec/workers/packages/debian/process_changes_worker_spec.rb'
+- './spec/workers/packages/go/sync_packages_worker_spec.rb'
+- './spec/workers/packages/helm/extraction_worker_spec.rb'
+- './spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb'
+- './spec/workers/packages/maven/metadata/sync_worker_spec.rb'
+- './spec/workers/packages/nuget/extraction_worker_spec.rb'
+- './spec/workers/packages/rubygems/extraction_worker_spec.rb'
+- './spec/workers/pages_domain_removal_cron_worker_spec.rb'
+- './spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb'
+- './spec/workers/pages_domain_ssl_renewal_worker_spec.rb'
+- './spec/workers/pages_domain_verification_cron_worker_spec.rb'
+- './spec/workers/pages_domain_verification_worker_spec.rb'
+- './spec/workers/pages/invalidate_domain_cache_worker_spec.rb'
+- './spec/workers/pages_worker_spec.rb'
+- './spec/workers/partition_creation_worker_spec.rb'
+- './spec/workers/personal_access_tokens/expired_notification_worker_spec.rb'
+- './spec/workers/personal_access_tokens/expiring_worker_spec.rb'
+- './spec/workers/pipeline_hooks_worker_spec.rb'
+- './spec/workers/pipeline_metrics_worker_spec.rb'
+- './spec/workers/pipeline_notification_worker_spec.rb'
+- './spec/workers/pipeline_process_worker_spec.rb'
+- './spec/workers/pipeline_schedule_worker_spec.rb'
+- './spec/workers/post_receive_spec.rb'
+- './spec/workers/process_commit_worker_spec.rb'
+- './spec/workers/project_cache_worker_spec.rb'
+- './spec/workers/project_destroy_worker_spec.rb'
+- './spec/workers/project_export_worker_spec.rb'
+- './spec/workers/projects/after_import_worker_spec.rb'
+- './spec/workers/projects/git_garbage_collect_worker_spec.rb'
+- './spec/workers/projects/import_export/relation_export_worker_spec.rb'
+- './spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb'
+- './spec/workers/projects/inactive_projects_deletion_notification_worker_spec.rb'
+- './spec/workers/projects/post_creation_worker_spec.rb'
+- './spec/workers/projects/process_sync_events_worker_spec.rb'
+- './spec/workers/projects/record_target_platforms_worker_spec.rb'
+- './spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb'
+- './spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb'
+- './spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb'
+- './spec/workers/projects/update_repository_storage_worker_spec.rb'
+- './spec/workers/propagate_integration_group_worker_spec.rb'
+- './spec/workers/propagate_integration_inherit_descendant_worker_spec.rb'
+- './spec/workers/propagate_integration_inherit_worker_spec.rb'
+- './spec/workers/propagate_integration_project_worker_spec.rb'
+- './spec/workers/propagate_integration_worker_spec.rb'
+- './spec/workers/prune_old_events_worker_spec.rb'
+- './spec/workers/purge_dependency_proxy_cache_worker_spec.rb'
+- './spec/workers/reactive_caching_worker_spec.rb'
+- './spec/workers/rebase_worker_spec.rb'
+- './spec/workers/releases/create_evidence_worker_spec.rb'
+- './spec/workers/releases/manage_evidence_worker_spec.rb'
+- './spec/workers/remote_mirror_notification_worker_spec.rb'
+- './spec/workers/remove_expired_group_links_worker_spec.rb'
+- './spec/workers/remove_expired_members_worker_spec.rb'
+- './spec/workers/remove_unaccepted_member_invites_worker_spec.rb'
+- './spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb'
+- './spec/workers/repository_check/batch_worker_spec.rb'
+- './spec/workers/repository_check/clear_worker_spec.rb'
+- './spec/workers/repository_check/dispatch_worker_spec.rb'
+- './spec/workers/repository_check/single_repository_worker_spec.rb'
+- './spec/workers/repository_cleanup_worker_spec.rb'
+- './spec/workers/repository_fork_worker_spec.rb'
+- './spec/workers/repository_import_worker_spec.rb'
+- './spec/workers/repository_update_remote_mirror_worker_spec.rb'
+- './spec/workers/run_pipeline_schedule_worker_spec.rb'
+- './spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb'
+- './spec/workers/schedule_migrate_external_diffs_worker_spec.rb'
+- './spec/workers/self_monitoring_project_create_worker_spec.rb'
+- './spec/workers/self_monitoring_project_delete_worker_spec.rb'
+- './spec/workers/service_desk_email_receiver_worker_spec.rb'
+- './spec/workers/snippets/schedule_bulk_repository_shard_moves_worker_spec.rb'
+- './spec/workers/snippets/update_repository_storage_worker_spec.rb'
+- './spec/workers/ssh_keys/expired_notification_worker_spec.rb'
+- './spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb'
+- './spec/workers/stage_update_worker_spec.rb'
+- './spec/workers/stuck_ci_jobs_worker_spec.rb'
+- './spec/workers/stuck_export_jobs_worker_spec.rb'
+- './spec/workers/stuck_merge_jobs_worker_spec.rb'
+- './spec/workers/system_hook_push_worker_spec.rb'
+- './spec/workers/tasks_to_be_done/create_worker_spec.rb'
+- './spec/workers/terraform/states/destroy_worker_spec.rb'
+- './spec/workers/todos_destroyer/confidential_issue_worker_spec.rb'
+- './spec/workers/todos_destroyer/destroyed_designs_worker_spec.rb'
+- './spec/workers/todos_destroyer/destroyed_issuable_worker_spec.rb'
+- './spec/workers/todos_destroyer/entity_leave_worker_spec.rb'
+- './spec/workers/todos_destroyer/group_private_worker_spec.rb'
+- './spec/workers/todos_destroyer/private_features_worker_spec.rb'
+- './spec/workers/todos_destroyer/project_private_worker_spec.rb'
+- './spec/workers/trending_projects_worker_spec.rb'
+- './spec/workers/update_container_registry_info_worker_spec.rb'
+- './spec/workers/update_external_pull_requests_worker_spec.rb'
+- './spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb'
+- './spec/workers/update_highest_role_worker_spec.rb'
+- './spec/workers/update_merge_requests_worker_spec.rb'
+- './spec/workers/update_project_statistics_worker_spec.rb'
+- './spec/workers/upload_checksum_worker_spec.rb'
+- './spec/workers/users/create_statistics_worker_spec.rb'
+- './spec/workers/users/deactivate_dormant_users_worker_spec.rb'
+- './spec/workers/user_status_cleanup/batch_worker_spec.rb'
+- './spec/workers/wait_for_cluster_creation_worker_spec.rb'
+- './spec/workers/web_hooks/log_destroy_worker_spec.rb'
+- './spec/workers/web_hook_worker_spec.rb'
+- './spec/workers/wikis/git_garbage_collect_worker_spec.rb'
+- './spec/workers/x509_certificate_revoke_worker_spec.rb'
+- './spec/workers/x509_issuer_crl_check_worker_spec.rb'
diff --git a/spec/support/seed.rb b/spec/support/seed.rb
deleted file mode 100644
index 36cb819763b..00000000000
--- a/spec/support/seed.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.configure do |config|
- config.include SeedHelper, :seed_helper
-
- config.before(:all, :seed_helper) do
- ensure_seeds
- end
-end
diff --git a/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb b/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb
index 5fcb14e075a..2f74d3131ab 100644
--- a/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb
+++ b/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb
@@ -25,15 +25,15 @@ RSpec.shared_context 'bulk imports requests context' do |url|
stub_request(:get, "https://gitlab.example.com/api/v4/groups?min_access_level=50&page=1&per_page=20&private_token=demo-pat&search=test&top_level_only=true")
.with(headers: request_headers)
.to_return(status: 200,
- body: [{
- id: 2595440,
- web_url: 'https://gitlab.com/groups/test',
- name: 'Test',
- path: 'stub-test-group',
- full_name: 'Test',
- full_path: 'stub-test-group'
- }].to_json,
- headers: page_response_headers)
+ body: [{
+ id: 2595440,
+ web_url: 'https://gitlab.com/groups/test',
+ name: 'Test',
+ path: 'stub-test-group',
+ full_name: 'Test',
+ full_path: 'stub-test-group'
+ }].to_json,
+ headers: page_response_headers)
stub_request(:get, "%{url}/api/v4/groups?min_access_level=50&page=1&per_page=20&private_token=demo-pat&search=&top_level_only=true" % { url: url })
.to_return(
diff --git a/spec/support/shared_contexts/design_management_shared_contexts.rb b/spec/support/shared_contexts/design_management_shared_contexts.rb
index e6ae7e03664..d89bcada1df 100644
--- a/spec/support/shared_contexts/design_management_shared_contexts.rb
+++ b/spec/support/shared_contexts/design_management_shared_contexts.rb
@@ -14,23 +14,23 @@ RSpec.shared_context 'four designs in three versions' do
let_it_be(:first_version) do
create(:design_version, issue: issue,
- created_designs: [design_a],
- modified_designs: [],
- deleted_designs: [])
+ created_designs: [design_a],
+ modified_designs: [],
+ deleted_designs: [])
end
let_it_be(:second_version) do
create(:design_version, issue: issue,
- created_designs: [design_b, design_c, design_d],
- modified_designs: [design_a],
- deleted_designs: [])
+ created_designs: [design_b, design_c, design_d],
+ modified_designs: [design_a],
+ deleted_designs: [])
end
let_it_be(:third_version) do
create(:design_version, issue: issue,
- created_designs: [],
- modified_designs: [design_a],
- deleted_designs: [design_d])
+ created_designs: [],
+ modified_designs: [design_a],
+ deleted_designs: [design_d])
end
before do
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 4c003dff947..91b6baac610 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -55,33 +55,33 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
let!(:merge_request1) do
create(:merge_request, assignees: [user], author: user, reviewers: [user2],
- source_project: project2, target_project: project1,
- target_branch: 'merged-target')
+ source_project: project2, target_project: project1,
+ target_branch: 'merged-target')
end
let!(:merge_request2) do
create(:merge_request, :conflict, assignees: [user], author: user, reviewers: [user2],
- source_project: project2, target_project: project1,
- state: 'closed')
+ source_project: project2, target_project: project1,
+ state: 'closed')
end
let!(:merge_request3) do
create(:merge_request, :simple, author: user, assignees: [user2], reviewers: [user],
- source_project: project2, target_project: project2,
- state: 'locked',
- title: 'thing WIP thing')
+ source_project: project2, target_project: project2,
+ state: 'locked',
+ title: 'thing WIP thing')
end
let!(:merge_request4) do
create(:merge_request, :simple, author: user,
- source_project: project3, target_project: project3,
- title: 'WIP thing')
+ source_project: project3, target_project: project3,
+ title: 'WIP thing')
end
let_it_be(:merge_request5) do
create(:merge_request, :simple, author: user,
- source_project: project4, target_project: project4,
- title: '[WIP]')
+ source_project: project4, target_project: project4,
+ title: '[WIP]')
end
let!(:label_link) { create(:label_link, label: label, target: merge_request2) }
diff --git a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb b/spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb
index 040b2da9f37..3623fa0850d 100644
--- a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
+++ b/spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb
@@ -1,31 +1,28 @@
# frozen_string_literal: true
-require 'spec_helper'
+require_relative '../../../../scripts/lib/glfm/constants'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec.
-# rubocop:disable Layout/LineLength
-RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_specification_dir|
- # rubocop:enable Layout/LineLength
- include ApiHelpers
-
- let_it_be(:user) { create(:user) }
- let_it_be(:api_url) { api('/markdown', user) }
+RSpec.shared_context 'with API::Markdown Snapshot shared context' do |ee_only: false|
+ include_context 'with GLFM example snapshot fixtures'
- markdown_examples, html_examples = %w[markdown.yml html.yml].map do |file_name|
- yaml = File.read("#{glfm_specification_dir}/example_snapshots/#{file_name}")
- YAML.safe_load(yaml, symbolize_names: true, aliases: true)
- end
+ include ApiHelpers
- normalizations_yaml = File.read(
- "#{glfm_specification_dir}/input/gitlab_flavored_markdown/glfm_example_normalizations.yml")
- normalizations_by_example_name = YAML.safe_load(normalizations_yaml, symbolize_names: true, aliases: true)
+ markdown_examples, html_examples, normalizations_by_example_name, metadata_by_example_name = [
+ Glfm::Constants::ES_MARKDOWN_YML_PATH,
+ Glfm::Constants::ES_HTML_YML_PATH,
+ Glfm::Constants::GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH,
+ Glfm::Constants::GLFM_EXAMPLE_METADATA_YML_PATH
+ ].map { |path| YAML.safe_load(File.open(path), symbolize_names: true, aliases: true) }
if (focused_markdown_examples_string = ENV['FOCUSED_MARKDOWN_EXAMPLES'])
focused_markdown_examples = focused_markdown_examples_string.split(',').map(&:strip).map(&:to_sym)
markdown_examples.select! { |example_name| focused_markdown_examples.include?(example_name) }
end
+ markdown_examples.select! { |example_name| !!metadata_by_example_name&.dig(example_name, :ee) == ee_only }
+
markdown_examples.each do |name, markdown|
context "for #{name}" do
let(:html) { html_examples.fetch(name).fetch(:static) }
@@ -34,13 +31,13 @@ RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_speci
it "verifies conversion of GLFM to HTML", :unlimited_max_formatted_output_length do
# noinspection RubyResolve
normalized_html = normalize_html(html, normalizations)
+ api_url = metadata_by_example_name&.dig(name, :api_request_override_path) || (api "/markdown")
post api_url, params: { text: markdown, gfm: true }
expect(response).to be_successful
- response_body = Gitlab::Json.parse(response.body)
- # Some requests have the HTML in the `html` key, others in the `body` key.
- response_html = response_body['body'] ? response_body.fetch('body') : response_body.fetch('html')
- # noinspection RubyResolve
+ parsed_response = Gitlab::Json.parse(response.body, symbolize_names: true)
+ # Some responses have the HTML in the `html` key, others in the `body` key.
+ response_html = parsed_response[:body] || parsed_response[:html]
normalized_response_html = normalize_html(response_html, normalizations)
expect(normalized_response_html).to eq(normalized_html)
diff --git a/spec/support/shared_contexts/glfm/example_snapshot_fixtures.rb b/spec/support/shared_contexts/glfm/example_snapshot_fixtures.rb
new file mode 100644
index 00000000000..22b401bc841
--- /dev/null
+++ b/spec/support/shared_contexts/glfm/example_snapshot_fixtures.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with GLFM example snapshot fixtures' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'glfm_group').tap { |group| group.add_owner(user) } }
+
+ let_it_be(:project) do
+ # NOTE: We hardcode the IDs on all fixtures to prevent variability in the
+ # rendered HTML/Prosemirror JSON, and to minimize the need for normalization:
+ # https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#normalization
+ create(:project, :repository, creator: user, group: group, name: 'glfm_project', id: 77777)
+ end
+
+ let_it_be(:project_snippet) { create(:project_snippet, title: 'glfm_project_snippet', id: 88888, project: project) }
+ let_it_be(:personal_snippet) { create(:snippet, id: 99999) }
+
+ before do
+ # Set 'GITLAB_TEST_FOOTNOTE_ID' in order to override random number generation in
+ # Banzai::Filter::FootnoteFilter#random_number, and thus avoid the need to
+ # perform normalization on the value. See:
+ # https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#normalization
+ stub_env('GITLAB_TEST_FOOTNOTE_ID', 42)
+
+ stub_licensed_features(group_wikis: true)
+ sign_in(user)
+ end
+end
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
index b29a231f3a6..d7cfdc09732 100644
--- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -16,8 +16,8 @@ RSpec.shared_context 'package details setup' do
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:first_file) { package.package_files.find { |f| a_graphql_entity_for(f).matches?(first_file_response) } }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
- let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
- let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
+ let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0) }
+ let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata) }
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
diff --git a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb
index dea03af2248..168aef0f174 100644
--- a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb
+++ b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb
@@ -42,7 +42,7 @@ RSpec.shared_context 'API::Markdown Golden Master shared context' do |markdown_y
if focused_markdown_examples_string = ENV['FOCUSED_MARKDOWN_EXAMPLES']
focused_markdown_examples = focused_markdown_examples_string.split(',').map(&:strip) || []
- markdown_examples.reject! {|markdown_example| !focused_markdown_examples.include?(markdown_example.fetch(:name)) }
+ markdown_examples.reject! { |markdown_example| !focused_markdown_examples.include?(markdown_example.fetch(:name)) }
end
markdown_examples.each do |markdown_example|
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 6c2ed79b343..064e40287be 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -109,8 +109,9 @@ RSpec.shared_context 'project navbar structure' do
_('Webhooks'),
_('Access Tokens'),
_('Repository'),
+ _('Merge requests'),
_('CI/CD'),
- _('Packages & Registries'),
+ _('Packages and registries'),
_('Monitor'),
s_('UsageQuota|Usage Quotas')
]
@@ -139,7 +140,17 @@ RSpec.shared_context 'group navbar structure' do
_('Repository'),
_('CI/CD'),
_('Applications'),
- _('Packages & Registries')
+ _('Packages and registries'),
+ _('Domain Verification')
+ ]
+ }
+ end
+
+ let(:settings_for_maintainer_nav_item) do
+ {
+ nav_item: _('Settings'),
+ nav_sub_items: [
+ _('Repository')
]
}
end
@@ -162,13 +173,6 @@ RSpec.shared_context 'group navbar structure' do
}
end
- let(:push_rules_nav_item) do
- {
- nav_item: _('Push Rules'),
- nav_sub_items: []
- }
- end
-
let(:ci_cd_nav_item) do
{
nav_item: _('CI/CD'),
@@ -210,7 +214,6 @@ RSpec.shared_context 'group navbar structure' do
nav_sub_items: []
},
(security_and_compliance_nav_item if Gitlab.ee?),
- (push_rules_nav_item if Gitlab.ee?),
{
nav_item: _('Kubernetes'),
nav_sub_items: []
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 1d4731d9b39..fc7255a4a20 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -6,13 +6,19 @@ RSpec.shared_context 'ProjectPolicy context' do
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
+ let_it_be(:inherited_guest) { create(:user) }
+ let_it_be(:inherited_reporter) { create(:user) }
+ let_it_be(:inherited_developer) { create(:user) }
+ let_it_be(:inherited_maintainer) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:non_member) { create(:user) }
+ let_it_be_with_refind(:group) { create(:group, :public) }
let_it_be_with_refind(:private_project) { create(:project, :private, namespace: owner.namespace) }
let_it_be_with_refind(:internal_project) { create(:project, :internal, namespace: owner.namespace) }
let_it_be_with_refind(:public_project) { create(:project, :public, namespace: owner.namespace) }
- let_it_be_with_refind(:public_project_in_group) { create(:project, :public, namespace: create(:group, :public)) }
+ let_it_be_with_refind(:public_project_in_group) { create(:project, :public, namespace: group) }
+ let_it_be_with_refind(:private_project_in_group) { create(:project, :private, namespace: group) }
let(:base_guest_permissions) do
%i[
@@ -95,6 +101,11 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:owner_permissions) { base_owner_permissions + additional_owner_permissions }
before_all do
+ group.add_guest(inherited_guest)
+ group.add_reporter(inherited_reporter)
+ group.add_developer(inherited_developer)
+ group.add_maintainer(inherited_maintainer)
+
[private_project, internal_project, public_project, public_project_in_group].each do |project|
project.add_guest(guest)
project.add_reporter(reporter)
diff --git a/spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb b/spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb
new file mode 100644
index 00000000000..c976bbe9212
--- /dev/null
+++ b/spec/support/shared_contexts/projects/container_repository/cleanup_tags_service_shared_context.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'for a cleanup tags service' do
+ def expected_service_response(status: :success, deleted: [], original_size: tags.size)
+ {
+ status: status,
+ deleted: deleted,
+ original_size: original_size,
+ before_delete_size: deleted&.size
+ }.compact.merge(deleted_size: deleted&.size)
+ end
+
+ def expect_delete(tags, container_expiration_policy: nil)
+ service = instance_double('Projects::ContainerRepository::DeleteTagsService')
+
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .to receive(:new)
+ .with(repository.project, user, tags: tags, container_expiration_policy: container_expiration_policy)
+ .and_return(service)
+
+ expect(service).to receive(:execute)
+ .with(repository) { { status: :success, deleted: tags } }
+ end
+
+ def expect_no_caching
+ expect(::Gitlab::Redis::Cache).not_to receive(:with)
+ end
+end
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
index b90270356f8..3974338238a 100644
--- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -64,5 +64,5 @@ RSpec.shared_context 'conan file upload endpoints' do
let(:jwt) { build_jwt(personal_access_token) }
let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_headers) }
- let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
+ let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz" }
end
diff --git a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
index 95b8b7ed9f8..cf090c7a185 100644
--- a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
@@ -18,6 +18,11 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') }
let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') }
let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) }
+ let_it_be(:private_component_file_sources) { create("debian_#{container_type}_component_file", :sources, component: private_component) }
+ let_it_be(:private_component_file_di) { create("debian_#{container_type}_component_file", :di_packages, component: private_component, architecture: private_architecture) }
+ let_it_be(:private_component_file_older_sha256) { create("debian_#{container_type}_component_file", :older_sha256, component: private_component, architecture: private_architecture) }
+ let_it_be(:private_component_file_sources_older_sha256) { create("debian_#{container_type}_component_file", :sources, :older_sha256, component: private_component) }
+ let_it_be(:private_component_file_di_older_sha256) { create("debian_#{container_type}_component_file", :di_packages, :older_sha256, component: private_component, architecture: private_architecture) }
let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') }
let_it_be(:public_distribution_key, freeze: true) { create("debian_#{container_type}_distribution_key", distribution: public_distribution) }
@@ -25,6 +30,11 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') }
let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') }
let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) }
+ let_it_be(:public_component_file_sources) { create("debian_#{container_type}_component_file", :sources, component: public_component) }
+ let_it_be(:public_component_file_di) { create("debian_#{container_type}_component_file", :di_packages, component: public_component, architecture: public_architecture) }
+ let_it_be(:public_component_file_older_sha256) { create("debian_#{container_type}_component_file", :older_sha256, component: public_component, architecture: public_architecture) }
+ let_it_be(:public_component_file_sources_older_sha256) { create("debian_#{container_type}_component_file", :sources, :older_sha256, component: public_component) }
+ let_it_be(:public_component_file_di_older_sha256) { create("debian_#{container_type}_component_file", :di_packages, :older_sha256, component: public_component, architecture: public_architecture) }
if container_type == :group
let_it_be(:private_project) { create(:project, :private, group: private_container) }
@@ -48,7 +58,9 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] }
let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] }
let(:component) { { private: private_component, public: public_component }[visibility_level] }
- let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] }
+ let(:component_file_older_sha256) { { private: private_component_file_older_sha256, public: public_component_file_older_sha256 }[visibility_level] }
+ let(:component_file_sources_older_sha256) { { private: private_component_file_sources_older_sha256, public: public_component_file_sources_older_sha256 }[visibility_level] }
+ let(:component_file_di_older_sha256) { { private: private_component_file_di_older_sha256, public: public_component_file_di_older_sha256 }[visibility_level] }
let(:package) { { private: private_package, public: public_package }[visibility_level] }
let(:letter) { package.name[0..2] == 'lib' ? package.name[0..3] : package.name[0] }
diff --git a/spec/support/shared_contexts/views/html_safe_render_shared_context.rb b/spec/support/shared_contexts/views/html_safe_render_shared_context.rb
new file mode 100644
index 00000000000..3acca60c901
--- /dev/null
+++ b/spec/support/shared_contexts/views/html_safe_render_shared_context.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when rendered view has no HTML escapes', type: :view do
+ # Check once per example if `rendered` contains HTML escapes.
+ let(:rendered) do |example|
+ super().tap do |rendered|
+ next if example.metadata[:skip_html_escaped_tags_check]
+
+ ensure_no_html_escaped_tags!(rendered, example)
+ end
+ end
+
+ def ensure_no_html_escaped_tags!(content, example)
+ match_data = HtmlEscapedHelpers.match_html_escaped_tags(content)
+ return unless match_data
+
+ # Truncate
+ pre_match = match_data.pre_match.last(50)
+ match = match_data[0]
+ post_match = match_data.post_match.first(50)
+
+ string = "#{pre_match}«#{match}»#{post_match}"
+
+ raise <<~MESSAGE
+ The following string contains HTML escaped tags:
+
+ #{string}
+
+ Please consider using `.html_safe`.
+
+ This check can be disabled via:
+
+ it #{example.description.inspect}, :skip_html_escaped_tags_check do
+ ...
+ end
+
+ MESSAGE
+ end
+end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 0e6f6f12c3f..fa048b76e18 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -125,6 +125,31 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests
end
+ it 'shows current board name' do
+ page.within('.boards-switcher') do
+ expect(page).to have_content(board.name)
+ end
+ end
+
+ it 'shows a list of boards' do
+ in_boards_switcher_dropdown do
+ expect(page).to have_content(board.name)
+ expect(page).to have_content(board2.name)
+ end
+ end
+
+ it 'switches current board' do
+ in_boards_switcher_dropdown do
+ click_button board2.name
+ end
+
+ wait_for_requests
+
+ page.within('.boards-switcher') do
+ expect(page).to have_content(board2.name)
+ end
+ end
+
it 'does not show action links' do
in_boards_switcher_dropdown do
expect(page).not_to have_content('Create new board')
diff --git a/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb b/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb
index 05b2b5f5de1..d8333ae25ad 100644
--- a/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb
+++ b/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb
@@ -8,14 +8,6 @@ RSpec.shared_examples 'editable job token scope' do
end
end
- context 'when job token scope is disabled for the given project' do
- before do
- allow(project).to receive(:ci_job_token_scope_enabled?).and_return(false)
- end
-
- it_behaves_like 'returns error', 'Job token scope is disabled for this project'
- end
-
context 'when user does not have permissions to edit the job token scope' do
it_behaves_like 'returns error', 'Insufficient permissions to modify the job token scope'
end
diff --git a/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb
new file mode 100644
index 00000000000..62c9c3508a8
--- /dev/null
+++ b/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples WebHooks::HookLogActions do
+ let!(:show_path) { web_hook_log.present.details_path }
+ let!(:retry_path) { web_hook_log.present.retry_path }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ it 'renders a 200 if the hook exists' do
+ get show_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('hook_logs/show')
+ end
+
+ it 'renders a 404 if the hook does not exist' do
+ web_hook.destroy!
+ get show_path
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'POST #retry' do
+ it 'executes the hook and redirects to the service form' do
+ stub_request(:post, web_hook.url)
+
+ expect_next_found_instance_of(web_hook.class) do |hook|
+ expect(hook).to receive(:execute).and_call_original
+ end
+
+ post retry_path
+
+ expect(response).to redirect_to(edit_hook_path)
+ end
+
+ it 'renders a 404 if the hook does not exist' do
+ web_hook.destroy!
+ post retry_path
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/error_tracking_shared_examples.rb b/spec/support/shared_examples/controllers/error_tracking_shared_examples.rb
index 08e5efcf63c..1bf2f158504 100644
--- a/spec/support/shared_examples/controllers/error_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/error_tracking_shared_examples.rb
@@ -3,5 +3,5 @@
RSpec.shared_examples 'sets the polling header' do
subject { response.headers[Gitlab::PollingInterval::HEADER_NAME] }
- it { is_expected.to eq '1000'}
+ it { is_expected.to eq '1000' }
end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 5faf462c23c..bbbe93a644f 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -241,12 +241,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
let(:provider_user) { double('user', login: provider_username) }
let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") }
let(:provider_repo) do
- double(
- 'provider',
+ {
name: 'vim',
full_name: "#{provider_username}/vim",
owner: double('owner', login: provider_username)
- )
+ }
end
before do
@@ -256,7 +255,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'returns 200 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -270,7 +269,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
project.errors.add(:path, 'is old')
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -281,7 +280,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "touches the etag cache store" do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" }
@@ -294,7 +293,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
context "when the provider user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -306,7 +305,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the current user's namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -331,7 +330,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the existing namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], existing_namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -343,7 +342,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
create(:user, username: provider_username)
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -357,15 +356,15 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: project))
- expect { post :create, params: { target_namespace: provider_repo.name }, format: :json }.to change(Namespace, :count).by(1)
+ expect { post :create, params: { target_namespace: provider_repo[:name] }, format: :json }.to change(Namespace, :count).by(1)
end
it "takes the new namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], an_instance_of(Group), user, type: provider, **access_params)
.and_return(double(execute: project))
- post :create, params: { target_namespace: provider_repo.name }, format: :json
+ post :create, params: { target_namespace: provider_repo[:name] }, format: :json
end
end
@@ -383,7 +382,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the current user's namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
+ .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
diff --git a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb
index aa4d78b23f4..112b9cbb204 100644
--- a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'snippets sort order' do
let(:params) { {} }
let(:sort_argument) { {} }
- let(:sort_params) { params.merge(sort_argument)}
+ let(:sort_params) { params.merge(sort_argument) }
before do
sign_in(user)
diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
index 2e691d1b36f..4af3c0cc6cc 100644
--- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
@@ -13,7 +13,7 @@
# - label
# - **extra
-shared_examples 'Snowplow event tracking' do |overrides: {}|
+RSpec.shared_examples 'Snowplow event tracking' do |overrides: {}|
let(:extra) { {} }
it 'is not emitted if FF is disabled' do
diff --git a/spec/support/shared_examples/features/board_sidebar_labels_examples.rb b/spec/support/shared_examples/features/board_sidebar_labels_examples.rb
index 520980c2615..4e5b371c18d 100644
--- a/spec/support/shared_examples/features/board_sidebar_labels_examples.rb
+++ b/spec/support/shared_examples/features/board_sidebar_labels_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_context 'labels from nested groups and projects' do
let_it_be(:maintainer) { create(:user) }
let(:labels_select) { find("[data-testid='sidebar-labels']") }
- let(:labels_dropdown) { labels_select.find('[data-testid="dropdown-content"]')}
+ let(:labels_dropdown) { labels_select.find('[data-testid="dropdown-content"]') }
before do
group.add_maintainer(maintainer)
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
index 8a07e52019c..f7cdc4c61ec 100644
--- a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
RSpec.shared_examples 'comment on merge request file' do
+ before do
+ stub_feature_flags(remove_user_attributes_projects: false)
+ end
+
it 'adds a comment' do
click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 3fa7beea97e..21f264a8b6a 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -1,22 +1,87 @@
# frozen_string_literal: true
RSpec.shared_examples 'edits content using the content editor' do
- content_editor_testid = '[data-testid="content-editor"] [contenteditable].ProseMirror'
+ let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
+
+ def switch_to_content_editor
+ find('[data-testid="toggle-editing-mode-button"] label', text: 'Rich text').click
+ end
+
+ def type_in_content_editor(keys)
+ find(content_editor_testid).send_keys keys
+ end
+
+ def open_insert_media_dropdown
+ page.find('svg[data-testid="media-icon"]').click
+ end
+
+ def set_source_editor_content(content)
+ find('.js-gfm-input').set content
+ end
+
+ def expect_formatting_menu_to_be_visible
+ expect(page).to have_css('[data-testid="formatting-bubble-menu"]')
+ end
+
+ def expect_formatting_menu_to_be_hidden
+ expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]')
+ end
+
+ def expect_media_bubble_menu_to_be_visible
+ expect(page).to have_css('[data-testid="media-bubble-menu"]')
+ end
+
+ def upload_asset(fixture_name)
+ attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true)
+ end
describe 'formatting bubble menu' do
- it 'shows a formatting bubble menu for a regular paragraph' do
+ it 'shows a formatting bubble menu for a regular paragraph and headings' do
+ switch_to_content_editor
+
expect(page).to have_css(content_editor_testid)
- find(content_editor_testid).send_keys 'Typing text in the content editor'
- find(content_editor_testid).send_keys [:shift, :left]
+ type_in_content_editor 'Typing text in the content editor'
+ type_in_content_editor [:shift, :left]
+
+ expect_formatting_menu_to_be_visible
+
+ type_in_content_editor [:right, :right, :enter, '## Heading']
- expect(page).to have_css('[data-testid="formatting-bubble-menu"]')
+ expect_formatting_menu_to_be_hidden
+
+ type_in_content_editor [:shift, :left]
+
+ expect_formatting_menu_to_be_visible
+ end
+ end
+
+ describe 'media elements bubble menu' do
+ before do
+ switch_to_content_editor
+
+ open_insert_media_dropdown
+ end
+
+ def test_displays_media_bubble_menu(media_element_selector, fixture_file)
+ upload_asset fixture_file
+
+ wait_for_requests
+
+ expect(page).to have_css(media_element_selector)
+
+ page.find(media_element_selector).click
+
+ expect_formatting_menu_to_be_hidden
+ expect_media_bubble_menu_to_be_visible
end
- it 'does not show a formatting bubble menu for code blocks' do
- find(content_editor_testid).send_keys '```js '
+ it 'displays correct media bubble menu for images', :js do
+ test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
+ end
- expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]')
+ it 'displays correct media bubble menu for video', :js do
+ test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
end
end
@@ -30,45 +95,50 @@ RSpec.shared_examples 'edits content using the content editor' do
page.go_back
refresh
+ switch_to_content_editor
end
it 'applies theme classes to code blocks' do
expect(page).not_to have_css('.content-editor-code-block.code.highlight.dark')
- find(content_editor_testid).send_keys [:enter, :enter]
- find(content_editor_testid).send_keys '```js ' # trigger input rule
- find(content_editor_testid).send_keys 'var a = 0'
+ type_in_content_editor [:enter, :enter]
+ type_in_content_editor '```js ' # trigger input rule
+ type_in_content_editor 'var a = 0'
expect(page).to have_css('.content-editor-code-block.code.highlight.dark')
end
end
describe 'code block bubble menu' do
+ before do
+ switch_to_content_editor
+ end
+
it 'shows a code block bubble menu for a code block' do
- find(content_editor_testid).send_keys [:enter, :enter]
+ type_in_content_editor [:enter, :enter]
- find(content_editor_testid).send_keys '```js ' # trigger input rule
- find(content_editor_testid).send_keys 'var a = 0'
- find(content_editor_testid).send_keys [:shift, :left]
+ type_in_content_editor '```js ' # trigger input rule
+ type_in_content_editor 'var a = 0'
+ type_in_content_editor [:shift, :left]
- expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]')
+ expect_formatting_menu_to_be_hidden
expect(page).to have_css('[data-testid="code-block-bubble-menu"]')
end
it 'sets code block type to "javascript" for `js`' do
- find(content_editor_testid).send_keys [:enter, :enter]
+ type_in_content_editor [:enter, :enter]
- find(content_editor_testid).send_keys '```js '
- find(content_editor_testid).send_keys 'var a = 0'
+ type_in_content_editor '```js '
+ type_in_content_editor 'var a = 0'
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Javascript')
end
it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do
- find(content_editor_testid).send_keys [:enter, :enter]
+ type_in_content_editor [:enter, :enter]
- find(content_editor_testid).send_keys '```nomnoml '
- find(content_editor_testid).send_keys 'test'
+ type_in_content_editor '```nomnoml '
+ type_in_content_editor 'test'
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)')
end
@@ -76,10 +146,11 @@ RSpec.shared_examples 'edits content using the content editor' do
describe 'mermaid diagram' do
before do
- find(content_editor_testid).send_keys [:enter, :enter]
+ switch_to_content_editor
- find(content_editor_testid).send_keys '```mermaid '
- find(content_editor_testid).send_keys ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34']
+ type_in_content_editor [:enter, :enter]
+ type_in_content_editor '```mermaid '
+ type_in_content_editor ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34']
end
it 'renders and updates the diagram correctly in a sandboxed iframe' do
diff --git a/spec/support/shared_examples/features/deploy_token_shared_examples.rb b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
index 25dfe089f51..79ad5bd6c7f 100644
--- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb
+++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
@@ -30,6 +30,27 @@ RSpec.shared_examples 'a deploy token in settings' do
expect(page).to have_selector("input[name='deploy-token-user'][value='deployer']")
expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']")
end
+
+ expect(find("input#deploy_token_name").value).to eq nil
+ expect(find("input#deploy_token_read_repository").checked?).to eq false
+ end
+
+ context "with form errors" do
+ before do
+ visit page_path
+ fill_in "deploy_token_name", with: "new_deploy_key"
+ fill_in "deploy_token_username", with: "deployer"
+ click_button "Create deploy token"
+ end
+
+ it "shows form errors" do
+ expect(page).to have_text("Scopes can't be blank")
+ end
+
+ it "keeps form inputs" do
+ expect(find("input#deploy_token_name").value).to eq "new_deploy_key"
+ expect(find("input#deploy_token_username").value).to eq "deployer"
+ end
end
context 'when User#time_display_relative is false', :js do
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 24dc4bcfc59..f209070d82a 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -304,7 +304,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] }
it 'can be replied to after resolving' do
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
wait_for_requests
refresh
@@ -316,7 +316,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
it 'shows resolved thread when toggled' do
submit_reply('a')
- find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
+ find('button[data-testid="resolve-discussion-button"]').click
wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true)
diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
index 442264e7ae4..b59f3f1e27b 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -5,39 +5,87 @@ RSpec.shared_examples 'manage applications' do
let_it_be(:application_name_changed) { "#{application_name} changed" }
let_it_be(:application_redirect_uri) { 'https://foo.bar' }
- it 'allows user to manage applications', :js do
- visit new_application_path
+ context 'when hash_oauth_secrets flag set' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: true)
+ end
+
+ it 'allows user to manage applications', :js do
+ visit new_application_path
- expect(page).to have_content 'Add new application'
+ expect(page).to have_content 'Add new application'
- fill_in :doorkeeper_application_name, with: application_name
- fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
- check :doorkeeper_application_scopes_read_user
- click_on 'Save application'
+ fill_in :doorkeeper_application_name, with: application_name
+ fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
+ check :doorkeeper_application_scopes_read_user
+ click_on 'Save application'
- validate_application(application_name, 'Yes')
- expect(page).to have_link('Continue', href: index_path)
+ validate_application(application_name, 'Yes')
+ expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely')
+ expect(page).to have_link('Continue', href: index_path)
- application = Doorkeeper::Application.find_by(name: application_name)
- expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy')
+ expect(page).to have_css("button[title=\"Copy secret\"]", text: 'Copy')
- click_on 'Edit'
+ click_on 'Edit'
- application_name_changed = "#{application_name} changed"
+ application_name_changed = "#{application_name} changed"
- fill_in :doorkeeper_application_name, with: application_name_changed
- uncheck :doorkeeper_application_confidential
- click_on 'Save application'
+ fill_in :doorkeeper_application_name, with: application_name_changed
+ uncheck :doorkeeper_application_confidential
+ click_on 'Save application'
+
+ validate_application(application_name_changed, 'No')
+ expect(page).not_to have_link('Continue')
+ expect(page).to have_content _('The secret is only available when you first create the application')
+
+ visit_applications_path
+
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
+ end
+ expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
+ end
+ end
+
+ context 'when hash_oauth_secrets flag not set' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
+
+ it 'allows user to manage applications', :js do
+ visit new_application_path
+
+ expect(page).to have_content 'Add new application'
+
+ fill_in :doorkeeper_application_name, with: application_name
+ fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
+ check :doorkeeper_application_scopes_read_user
+ click_on 'Save application'
+
+ validate_application(application_name, 'Yes')
+ expect(page).to have_link('Continue', href: index_path)
+
+ application = Doorkeeper::Application.find_by(name: application_name)
+ expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy')
+
+ click_on 'Edit'
+
+ application_name_changed = "#{application_name} changed"
+
+ fill_in :doorkeeper_application_name, with: application_name_changed
+ uncheck :doorkeeper_application_confidential
+ click_on 'Save application'
- validate_application(application_name_changed, 'No')
- expect(page).not_to have_link('Continue')
+ validate_application(application_name_changed, 'No')
+ expect(page).not_to have_link('Continue')
- visit_applications_path
+ visit_applications_path
- page.within '.oauth-applications' do
- click_on 'Destroy'
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
+ end
+ expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
end
- expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
end
context 'when scopes are blank' do
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 323bd4f5171..7aad5e2de80 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
def package_table_row(index)
- page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text # rubocop:disable QA/SelectorUsage
+ page.all("#{packages_table_selector} > [data-testid=\"package-row\"]")[index].text
end
end
@@ -84,7 +84,7 @@ RSpec.shared_examples 'shared package sorting' do
end
def packages_table_selector
- '[data-qa-selector="packages-table"]' # rubocop:disable QA/SelectorUsage
+ '[data-testid="packages-table"]'
end
def click_sort_option(option, ascending)
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
index 8212f14d6be..81d548e000a 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
find(".js-allowed-to-merge").click
- within('.rspec-allowed-to-merge-dropdown') do
+ within('[data-testid="allowed-to-merge-dropdown"]') do
expect(first("li")).to have_content("Roles")
find(:link, 'No one').click
end
@@ -35,13 +35,13 @@ RSpec.shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
find(".js-allowed-to-merge").click
- within('.rspec-allowed-to-merge-dropdown') do
+ within('[data-testid="allowed-to-merge-dropdown"]') do
expect(first("li")).to have_content("Roles")
find(:link, 'No one').click
end
find(".js-allowed-to-push").click
- within('.rspec-allowed-to-push-dropdown') do
+ within('[data-testid="allowed-to-push-dropdown"]') do
expect(first("li")).to have_content("Roles")
find(:link, 'No one').click
end
@@ -83,7 +83,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end
find(".js-allowed-to-push").click
- within('.rspec-allowed-to-push-dropdown') do
+ within('[data-testid="allowed-to-push-dropdown"]') do
expect(first("li")).to have_content("Roles")
find(:link, 'No one').click
end
@@ -100,13 +100,13 @@ RSpec.shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
find(".js-allowed-to-merge").click
- within('.rspec-allowed-to-merge-dropdown') do
+ within('[data-testid="allowed-to-merge-dropdown"]') do
expect(first("li")).to have_content("Roles")
find(:link, 'No one').click
end
find(".js-allowed-to-push").click
- within('.rspec-allowed-to-push-dropdown') do
+ within('[data-testid="allowed-to-push-dropdown"]') do
expect(first("li")).to have_content("Roles")
find(:link, 'No one').click
end
diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
index 14142793a0d..90b0e600228 100644
--- a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
@@ -23,7 +23,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
find(".js-allowed-to-push").click
wait_for_requests
- within('.qa-allowed-to-push-dropdown') do # rubocop:disable QA/SelectorUsage
+ within('[data-testid="allowed-to-push-dropdown"]') do
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
@@ -38,7 +38,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
find(".js-allowed-to-merge").click
wait_for_requests
- within('.qa-allowed-to-merge-dropdown') do # rubocop:disable QA/SelectorUsage
+ within('[data-testid="allowed-to-merge-dropdown"]') do
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
@@ -68,7 +68,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
find(".js-allowed-to-push").click
wait_for_requests
- within('.qa-allowed-to-push-dropdown') do # rubocop:disable QA/SelectorUsage
+ within('[data-testid="allowed-to-push-dropdown"]') do
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb
index 0991de21d8d..ad865b084e1 100644
--- a/spec/support/shared_examples/features/rss_shared_examples.rb
+++ b/spec/support/shared_examples/features/rss_shared_examples.rb
@@ -9,7 +9,7 @@ end
RSpec.shared_examples "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
expect(page)
- .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']") # rubocop:disable QA/SelectorUsage
+ .to have_css("a:has([data-testid='rss-icon'])[href*='feed_token=#{user.feed_token}']")
end
end
@@ -22,7 +22,7 @@ end
RSpec.shared_examples "it has an RSS button without a feed token" do
it "shows the RSS button without a feed token" do
expect(page)
- .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") # rubocop:disable QA/SelectorUsage
+ .to have_css("a:has([data-testid='rss-icon']):not([href*='feed_token'])")
end
end
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 52f3fd60c07..31ee08ea9db 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -64,9 +64,9 @@ end
RSpec.shared_examples 'shows no runners registered' do
it 'shows counts with 0' do
- expect(page).to have_text "Online runners 0"
- expect(page).to have_text "Offline runners 0"
- expect(page).to have_text "Stale runners 0"
+ expect(page).to have_text "#{s_('Runners|Online')} 0"
+ expect(page).to have_text "#{s_('Runners|Offline')} 0"
+ expect(page).to have_text "#{s_('Runners|Stale')} 0"
end
it 'shows "no runners" message' do
@@ -101,7 +101,7 @@ RSpec.shared_examples 'pauses, resumes and deletes a runner' do
within_runner_row(runner.id) do
click_button "Pause"
- expect(page).to have_text 'paused'
+ expect(page).to have_text s_('Runners|Paused')
expect(page).to have_button 'Resume'
expect(page).not_to have_button 'Pause'
@@ -145,3 +145,39 @@ RSpec.shared_examples 'pauses, resumes and deletes a runner' do
end
end
end
+
+RSpec.shared_examples 'submits edit runner form' do
+ it 'breadcrumb contains runner id and token' do
+ page.within '[data-testid="breadcrumb-links"]' do
+ expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
+ expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_content("Edit")
+ end
+ end
+
+ describe 'runner header', :js do
+ it 'contains the runner id' do
+ expect(page).to have_content("Runner ##{runner.id} created")
+ end
+ end
+
+ context 'when a runner is updated', :js do
+ before do
+ find('[data-testid="runner-field-description"] input').set('new-runner-description')
+
+ click_on _('Save changes')
+ wait_for_requests
+ end
+
+ it 'redirects to runner page' do
+ expect(current_url).to match(runner_page_path)
+ end
+
+ it 'show success alert' do
+ expect(page.find('[data-testid="alert-success"]')).to have_content('saved')
+ end
+
+ it 'shows updated information' do
+ expect(page).to have_content("#{s_('Runners|Description')} new-runner-description")
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb
index c402333107c..bf870b3ce66 100644
--- a/spec/support/shared_examples/features/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/features/snippets_shared_examples.rb
@@ -194,7 +194,7 @@ end
RSpec.shared_examples 'personal snippet with references' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
- let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project)}
+ let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:commit) { project.commit }
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 9d81c0e9a3e..d1e5046a39e 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -91,7 +91,7 @@ RSpec.shared_examples 'variable list' do |is_admin|
end
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set('new_key') # rubocop:disable QA/SelectorUsage
+ find('[data-testid="pipeline-form-ci-variable-key"] input').set('new_key')
click_button('Update variable')
end
@@ -173,7 +173,7 @@ RSpec.shared_examples 'variable list' do |is_admin|
click_button('Add variable')
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') # rubocop:disable QA/SelectorUsage
+ find('[data-testid="pipeline-form-ci-variable-key"] input').set('empty_mask_key')
find('[data-testid="ci-variable-protected-checkbox"]').click
find('[data-testid="ci-variable-masked-checkbox"]').click
@@ -290,8 +290,8 @@ RSpec.shared_examples 'variable list' do |is_admin|
wait_for_requests
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set(key) # rubocop:disable QA/SelectorUsage
- find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present? # rubocop:disable QA/SelectorUsage
+ find('[data-testid="pipeline-form-ci-variable-key"] input').set(key)
+ find('[data-testid="pipeline-form-ci-variable-value"]').set(value) if value.present?
find('[data-testid="ci-variable-protected-checkbox"]').click if protected
find('[data-testid="ci-variable-masked-checkbox"]').click if masked
diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
index 2285d9a17e2..3e285bb8ad7 100644
--- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
@@ -64,7 +64,7 @@ RSpec.shared_examples 'User previews wiki changes' do
end
it_behaves_like 'relative links' do
- let(:element) { page.find('[data-testid="wiki_page_content"]') }
+ let(:element) { page.find('[data-testid="wiki-page-content"]') }
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index 87067336a36..5c63d6a973d 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -137,16 +137,7 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
- context 'when using the content editor' do
- context 'with feature flag on' do
- before do
- find('[data-testid="toggle-editing-mode-button"] label', text: 'Rich text').click
- end
-
- it_behaves_like 'edits content using the content editor'
- end
- end
-
+ it_behaves_like 'edits content using the content editor'
it_behaves_like 'autocompletes items'
end
diff --git a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
index 6fdc5ecae73..fde38df558f 100644
--- a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'User views AsciiDoc page with includes' do
- let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' } # rubocop:disable QA/SelectorUsage
- let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page')}
+ let_it_be(:wiki_content_selector) { '[data-testid=wiki-page-content]' }
+ let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page') }
let!(:wiki_page) { create_wiki_page('home', content: "Content from the main page.\ninclude::included_page.asciidoc[]") }
def create_wiki_page(title, content:)
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 049ead9fb89..f62c9c00006 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -365,7 +365,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
let!(:created_items) do
milestones.map do |milestone|
create(factory, project: milestone.project || project_in_group,
- milestone: milestone, author: user, assignees: [user])
+ milestone: milestone, author: user, assignees: [user])
end
end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index 110706c730b..5cba8baa829 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -40,7 +40,7 @@ RSpec.shared_examples 'querying members with a group' do
subject do
resolve(described_class, obj: resource, args: base_args.merge(args),
- ctx: { current_user: user_4 }, arg_style: :internal)
+ ctx: { current_user: user_4 }, arg_style: :internal)
end
describe '#resolve' do
@@ -52,6 +52,15 @@ RSpec.shared_examples 'querying members with a group' do
expect(subject).to contain_exactly(resource_member, group_1_member, root_group_member)
end
+ context 'with sort options' do
+ let(:args) { { sort: 'name_asc' } }
+
+ it 'searches users by user name' do
+ # the order is important here
+ expect(subject.items).to eq([root_group_member, resource_member, group_1_member])
+ end
+ end
+
context 'with search' do
context 'when the search term matches a user' do
let(:args) { { search: 'test' } }
@@ -75,7 +84,7 @@ RSpec.shared_examples 'querying members with a group' do
subject do
resolve(described_class, obj: resource, args: base_args.merge(args),
- ctx: { current_user: other_user }, arg_style: :internal)
+ ctx: { current_user: other_user }, arg_style: :internal)
end
it 'generates an error' do
diff --git a/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
index 738edd43c92..faf1bb204c9 100644
--- a/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
+++ b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-shared_examples 'N+1 query check' do
+RSpec.shared_examples 'N+1 query check' do
it 'prevents N+1 queries' do
execute_query # "warm up" to prevent undeterministic counts
expect(graphql_errors).to be_blank # Sanity check - ex falso quodlibet!
diff --git a/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb
new file mode 100644
index 00000000000..25008bca619
--- /dev/null
+++ b/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+# Requires `parent`, issuable1`, `issuable2`, `issuable3`, `issuable4`,
+# `finder_class` and `optimization_param` bindings.
+RSpec.shared_examples 'graphql query for searching issuables' do
+ it 'uses search optimization' do
+ expected_arguments = a_hash_including(
+ search: 'text',
+ optimization_param => true
+ )
+ expect(finder_class).to receive(:new).with(anything, expected_arguments).and_call_original
+
+ resolve_issuables(search: 'text')
+ end
+
+ it 'filters issuables by title' do
+ issuables = resolve_issuables(search: 'created')
+
+ expect(issuables).to contain_exactly(issuable1, issuable2)
+ end
+
+ it 'filters issuables by description' do
+ issuables = resolve_issuables(search: 'text')
+
+ expect(issuables).to contain_exactly(issuable2, issuable3)
+ end
+
+ context 'with in param' do
+ it 'generates an error if param search is missing' do
+ error_message = "`search` should be present when including the `in` argument"
+
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, error_message) do
+ resolve_issuables(in: ['title'])
+ end
+ end
+
+ it 'filters issuables by title and description' do
+ issuable4.update!(title: 'fourth text')
+ issuables = resolve_issuables(search: 'text', in: %w[title description])
+
+ expect(issuables).to contain_exactly(issuable2, issuable3, issuable4)
+ end
+
+ it 'filters issuables by description only' do
+ with_text = resolve_issuables(search: 'text', in: ['description'])
+ with_created = resolve_issuables(search: 'created', in: ['description'])
+
+ expect(with_created).to be_empty
+ expect(with_text).to contain_exactly(issuable2, issuable3)
+ end
+
+ it 'filters issuables by title only' do
+ with_text = resolve_issuables(search: 'text', in: ['title'])
+ with_created = resolve_issuables(search: 'created', in: ['title'])
+
+ expect(with_created).to contain_exactly(issuable1, issuable2)
+ expect(with_text).to be_empty
+ end
+ end
+
+ context 'with anonymous user' do
+ let_it_be(:current_user) { nil }
+
+ context 'with disable_anonymous_search as `true`' do
+ before do
+ stub_feature_flags(disable_anonymous_search: true)
+ end
+
+ it 'returns an error' do
+ error_message = "User must be authenticated to include the `search` argument."
+
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, error_message) do
+ resolve_issuables(search: 'created')
+ end
+ end
+
+ it 'does not return error if search term is not present' do
+ expect(resolve_issuables).not_to be_instance_of(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
+ context 'with disable_anonymous_search as `false`' do
+ before do
+ stub_feature_flags(disable_anonymous_search: false)
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'filters issuables by search term' do
+ issuables = resolve_issuables(search: 'created')
+
+ expect(issuables).to contain_exactly(issuable1, issuable2)
+ end
+ end
+ end
+
+ def resolve_issuables(args = {}, obj = parent, context = { current_user: current_user })
+ resolve(described_class, obj: obj, args: args, ctx: context, arg_style: :internal)
+ end
+end
diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
index b5c07f45d59..47655f86558 100644
--- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
@@ -45,62 +45,62 @@ RSpec.shared_examples 'XSS prevention' do
# Adapted from the Sanitize test suite: http://git.io/vczrM
protocols = {
'protocol-based JS injection: simple, no spaces' => {
- input: '<a href="javascript:alert(\'XSS\');">foo</a>',
+ input: '<a href="javascript:alert(\'XSS\');">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: simple, spaces before' => {
- input: '<a href="javascript :alert(\'XSS\');">foo</a>',
+ input: '<a href="javascript :alert(\'XSS\');">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: simple, spaces after' => {
- input: '<a href="javascript: alert(\'XSS\');">foo</a>',
+ input: '<a href="javascript: alert(\'XSS\');">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: simple, spaces before and after' => {
- input: '<a href="javascript : alert(\'XSS\');">foo</a>',
+ input: '<a href="javascript : alert(\'XSS\');">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: preceding colon' => {
- input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
+ input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: UTF-8 encoding' => {
- input: '<a href="javascript&#58;">foo</a>',
+ input: '<a href="javascript&#58;">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: long UTF-8 encoding' => {
- input: '<a href="javascript&#0058;">foo</a>',
+ input: '<a href="javascript&#0058;">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: long UTF-8 encoding without semicolons' => {
- input: '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+ input: '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: hex encoding' => {
- input: '<a href="javascript&#x3A;">foo</a>',
+ input: '<a href="javascript&#x3A;">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: long hex encoding' => {
- input: '<a href="javascript&#x003A;">foo</a>',
+ input: '<a href="javascript&#x003A;">foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: hex encoding without semicolons' => {
- input: '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
+ input: '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
output: '<a>foo</a>'
},
'protocol-based JS injection: null char' => {
- input: "<a href=java\0script:alert(\"XSS\")>foo</a>",
+ input: "<a href=java\0script:alert(\"XSS\")>foo</a>",
output: '<a href="java"></a>'
},
@@ -115,7 +115,7 @@ RSpec.shared_examples 'XSS prevention' do
},
'protocol-based JS injection: spaces and entities' => {
- input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
+ input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>'
},
diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
index 845fa78a827..82a9e8130f7 100644
--- a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
+++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
@@ -43,6 +43,54 @@ RSpec.shared_examples_for 'object cache helper' do
subject
end
end
+
+ context 'when a caller id is present' do
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) }
+ let(:caller_id) { 'caller_id' }
+
+ before do
+ allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction)
+ allow(transaction).to receive(:increment)
+ allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id)
+ end
+
+ context 'when feature flag is off' do
+ before do
+ stub_feature_flags(add_timing_to_certain_cache_actions: false)
+ end
+
+ it 'does not call increment' do
+ expect(transaction).not_to receive(:increment).with(:cached_object_operations_total, any_args)
+
+ subject
+ end
+
+ it 'does not call histogram' do
+ expect(Gitlab::Metrics).not_to receive(:histogram)
+
+ subject
+ end
+
+ it "is valid JSON" do
+ parsed = Gitlab::Json.parse(subject.to_s)
+
+ expect(parsed).to be_a(Hash)
+ expect(parsed["id"]).to eq(presentable.id)
+ end
+ end
+
+ it 'increments the counter' do
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, 1, { caller_id: caller_id, render_type: :object, cache_hit: false }).once
+
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, 0, { caller_id: caller_id, render_type: :object, cache_hit: true }).once
+
+ subject
+ end
+ end
end
RSpec.shared_examples_for 'collection cache helper' do
@@ -98,4 +146,95 @@ RSpec.shared_examples_for 'collection cache helper' do
subject
end
end
+
+ context 'when a caller id is present' do
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) }
+ let(:caller_id) { 'caller_id' }
+
+ before do
+ allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction)
+ allow(transaction).to receive(:increment)
+ allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id)
+ end
+
+ context 'when feature flag is off' do
+ before do
+ stub_feature_flags(add_timing_to_certain_cache_actions: false)
+ end
+
+ it 'does not call increment' do
+ expect(transaction).not_to receive(:increment).with(:cached_object_operations_total, any_args)
+
+ subject
+ end
+
+ it 'does not call histogram' do
+ expect(Gitlab::Metrics).not_to receive(:histogram)
+
+ subject
+ end
+
+ it "is valid JSON" do
+ parsed = Gitlab::Json.parse(subject.to_s)
+
+ expect(parsed).to be_an(Array)
+
+ presentable.each_with_index do |item, i|
+ expect(parsed[i]["id"]).to eq(item.id)
+ end
+ end
+ end
+
+ context 'when presentable has a group by clause' do
+ let(:presentable) { MergeRequest.group(:id) }
+
+ it "returns the presentables" do
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, 0, { caller_id: caller_id, render_type: :collection, cache_hit: true }).once
+
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, MergeRequest.count, { caller_id: caller_id, render_type: :collection, cache_hit: false }).once
+
+ parsed = Gitlab::Json.parse(subject.to_s)
+
+ expect(parsed).to be_an(Array)
+
+ presentable.each_with_index do |item, i|
+ expect(parsed[i]["id"]).to eq(item.id)
+ end
+ end
+ end
+
+ context 'when the presentables all miss' do
+ it 'increments the counters' do
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, 0, { caller_id: caller_id, render_type: :collection, cache_hit: true }).once
+
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, presentable.size, { caller_id: caller_id, render_type: :collection, cache_hit: false }).once
+
+ subject
+ end
+ end
+
+ context 'when the presents hit' do
+ it 'increments the counters' do
+ subject
+
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, presentable.size, { caller_id: caller_id, render_type: :collection, cache_hit: true }).once
+
+ expect(transaction)
+ .to receive(:increment)
+ .with(:cached_object_operations_total, 0, { caller_id: caller_id, render_type: :collection, cache_hit: false }).once
+
+ instance.public_send(method, presentable, **kwargs)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
index b786d7e5527..10f58748698 100644
--- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
@@ -131,7 +131,7 @@ RSpec.shared_examples 'common trace features' do
end
context 'logs contains "section_start"' do
- let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
+ let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K" }
it "returns only one section" do
expect(sections).not_to be_empty
@@ -144,7 +144,7 @@ RSpec.shared_examples 'common trace features' do
end
context 'missing section_end' do
- let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
+ let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n" }
it "returns no sections" do
expect(sections).to be_empty
@@ -152,7 +152,7 @@ RSpec.shared_examples 'common trace features' do
end
context 'missing section_start' do
- let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
+ let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K" }
it "returns no sections" do
expect(sections).to be_empty
@@ -160,7 +160,7 @@ RSpec.shared_examples 'common trace features' do
end
context 'inverted section_start section_end' do
- let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
+ let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K" }
it "returns no sections" do
expect(sections).to be_empty
@@ -169,7 +169,7 @@ RSpec.shared_examples 'common trace features' do
end
describe '#write' do
- subject { trace.send(:write, mode) { } }
+ subject { trace.send(:write, mode) {} }
let(:mode) { 'wb' }
@@ -370,15 +370,6 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
end
end
- shared_examples 'read successfully with StringIO' do
- it 'yields with source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_a(StringIO)
- end
- end
- end
-
shared_examples 'failed to read' do
it 'yields without source' do
trace.read do |stream|
@@ -404,14 +395,6 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
it_behaves_like 'read successfully with IO'
end
- context 'when db trace exists' do
- before do
- build.send(:write_attribute, :trace, "data")
- end
-
- it_behaves_like 'read successfully with StringIO'
- end
-
context 'when no sources exist' do
it_behaves_like 'failed to read'
end
@@ -462,25 +445,6 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
expect(trace.exist?).to be(false)
end
end
-
- context 'stored in database' do
- before do
- build.send(:write_attribute, :trace, "data")
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
-
- it "returns database data" do
- expect(trace.raw).to eq("data")
- end
- end
end
describe '#archive!' do
@@ -520,24 +484,12 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
expect(build.trace.exist?).to be_truthy
expect(build.job_artifacts_trace.file.exists?).to be_truthy
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
- expect(build.old_trace).to be_nil
expect(src_checksum)
.to eq(described_class.sha256_hexdigest(build.job_artifacts_trace.file.path))
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
end
end
- shared_examples 'source trace in database stays intact' do |error:|
- it do
- expect { subject }.to raise_error(error)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace).to be_nil
- expect(build.old_trace).to eq(trace_content)
- end
- end
-
context 'when job does not have trace artifact' do
context 'when trace file stored in default path' do
let!(:build) { create(:ci_build, :success, :trace_live) }
@@ -564,58 +516,6 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
end
end
-
- context 'when trace is stored in database' do
- let(:build) { create(:ci_build, :success) }
- let(:trace_content) { 'Sample trace' }
- let(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
-
- before do
- build.update_column(:trace, trace_content)
- end
-
- it_behaves_like 'archive trace in database'
-
- context 'when failed to create clone file' do
- before do
- allow(IO).to receive(:copy_stream).and_return(0)
- end
-
- it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
- end
-
- context 'when failed to create job artifact record' do
- before do
- allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
- end
-
- context 'when there is a validation error on Ci::Build' do
- before do
- allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- context "when erase old trace with 'save'" do
- before do
- build.send(:write_attribute, :trace, nil)
- build.save # rubocop:disable Rails/SaveBang
- end
-
- it 'old trace is not deleted' do
- build.reload
- expect(build.trace.raw).to eq(trace_content)
- end
- end
-
- it_behaves_like 'archive trace in database'
- end
- end
end
context 'when job has trace artifact' do
@@ -645,22 +545,6 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
subject { trace.erase! }
context 'when it is a live trace' do
- context 'when trace is stored in database' do
- let(:build) { create(:ci_build) }
-
- before do
- build.update_column(:trace, 'sample trace')
- end
-
- it { expect(trace.raw).not_to be_nil }
-
- it "removes trace" do
- subject
-
- expect(trace.raw).to be_nil
- end
- end
-
context 'when trace is stored in file storage' do
let(:build) { create(:ci_build, :trace_live) }
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
index beec072e474..9ffc55f7e7e 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples 'deployment metrics examples' do
+RSpec.shared_examples 'deployment metrics examples' do
def create_deployment(args)
project = args[:project]
environment = project.environments.production.first || create(:environment, :production, project: project)
diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
index 771ab89972c..a28fefcfc58 100644
--- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'marks background migration job records' do
it 'marks each job record as succeeded after processing' do
create(:background_migration_job, class_name: "::#{described_class.name.demodulize}",
- arguments: arguments)
+ arguments: arguments)
expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original
@@ -14,7 +14,7 @@ RSpec.shared_examples 'marks background migration job records' do
it 'returns the number of job records marked as succeeded' do
create(:background_migration_job, class_name: "::#{described_class.name.demodulize}",
- arguments: arguments)
+ arguments: arguments)
jobs_updated = subject.perform(*arguments)
diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
index 1f7325df11a..243dc1d195b 100644
--- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
@@ -144,7 +144,7 @@ RSpec.shared_examples 'cacheable diff collection' do
end
end
-shared_examples_for 'sortable diff files' do
+RSpec.shared_examples_for 'sortable diff files' do
subject { described_class.new(diffable, **collection_default_args) }
describe '#raw_diff_files' do
@@ -170,7 +170,7 @@ shared_examples_for 'sortable diff files' do
end
end
-shared_examples_for 'unsortable diff files' do
+RSpec.shared_examples_for 'unsortable diff files' do
subject { described_class.new(diffable, **collection_default_args) }
describe '#raw_diff_files' do
diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
index ead8b174d46..ec7b2794703 100644
--- a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword|
empty_relation = User.none.select(:id)
set_operator = described_class.new([empty_relation, relation_1, relation_2])
- expect {User.where("users.id IN (#{set_operator.to_sql})").to_a}.not_to raise_error
+ expect { User.where("users.id IN (#{set_operator.to_sql})").to_a }.not_to raise_error
expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})")
end
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index d73c7b6848d..1c0e0061385 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -43,7 +43,7 @@ RSpec.shared_examples 'maps Sentry exceptions' do |http_method|
}
exceptions.each do |exception, message|
- context "#{exception}" do
+ context exception do
before do
stub_request(
http_method || :get,
@@ -58,3 +58,50 @@ RSpec.shared_examples 'maps Sentry exceptions' do |http_method|
end
end
end
+
+# Expects to following variables:
+# - subject
+# - sentry_api_response
+# - sentry_url, token - only if enabled_by_default: false
+RSpec.shared_examples 'Sentry API response size limit' do |enabled_by_default: false|
+ let(:invalid_deep_size) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
+
+ before do
+ allow(Gitlab::Utils::DeepSize)
+ .to receive(:new)
+ .with(sentry_api_response, any_args)
+ .and_return(invalid_deep_size)
+ end
+
+ if enabled_by_default
+ it 'raises an exception when response is too large' do
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
+ 'Sentry API response is too big. Limit is 1 MB.')
+ end
+ else
+ context 'when guarded by feature flag' do
+ let(:client) do
+ ErrorTracking::SentryClient.new(sentry_url, token, validate_size_guarded_by_feature_flag: feature_flag)
+ end
+
+ context 'with feature flag enabled' do
+ let(:feature_flag) { true }
+
+ it 'raises an exception when response is too large' do
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
+ 'Sentry API response is too big. Limit is 1 MB.')
+ end
+ end
+
+ context 'with feature flag disabled' do
+ let(:feature_flag) { false }
+
+ it 'does not check the limit and thus not raise' do
+ expect { subject }.not_to raise_error
+
+ expect(Gitlab::Utils::DeepSize).not_to have_received(:new)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index fb08784f34f..6cfeeabc952 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -32,9 +32,11 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
describe "#execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+
let(:webhook_url) { "https://example.gitlab.com/" }
+ let(:webhook_url_regex) { /\A#{webhook_url}.*/ }
before do
allow(subject).to receive_messages(
@@ -44,7 +46,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
webhook: webhook_url
)
- WebMock.stub_request(:post, webhook_url)
+ WebMock.stub_request(:post, webhook_url_regex)
end
shared_examples "triggered #{integration_name} integration" do |branches_to_be_notified: nil|
@@ -56,7 +58,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
result = subject.execute(sample_data)
expect(result).to be(true)
- expect(WebMock).to have_requested(:post, webhook_url).once.with { |req|
+ expect(WebMock).to have_requested(:post, webhook_url_regex).once.with { |req|
json_body = Gitlab::Json.parse(req.body).with_indifferent_access
expect(json_body).to include(payload)
}
@@ -72,7 +74,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
result = subject.execute(sample_data)
expect(result).to be_falsy
- expect(WebMock).not_to have_requested(:post, webhook_url)
+ expect(WebMock).not_to have_requested(:post, webhook_url_regex)
end
end
@@ -112,14 +114,14 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context "with protected branch" do
- before do
- create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch")
- end
-
let(:sample_data) do
Gitlab::DataBuilder::Push.build(project: project, user: user, ref: "a-protected-branch")
end
+ before_all do
+ create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch")
+ end
+
context "when only default branch are to be notified" do
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
@@ -214,7 +216,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
let(:sample_data) { Gitlab::DataBuilder::Note.build(note, user) }
context "with commit comment" do
- let(:note) do
+ let_it_be(:note) do
create(:note_on_commit,
author: user,
project: project,
@@ -226,7 +228,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context "with merge request comment" do
- let(:note) do
+ let_it_be(:note) do
create(:note_on_merge_request, project: project, note: "merge request note")
end
@@ -234,7 +236,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context "with issue comment" do
- let(:note) do
+ let_it_be(:note) do
create(:note_on_issue, project: project, note: "issue note")
end
@@ -242,7 +244,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context "with snippet comment" do
- let(:note) do
+ let_it_be(:note) do
create(:note_on_project_snippet, project: project, note: "snippet note")
end
@@ -251,22 +253,24 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context "with pipeline events" do
- let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: status,
- sha: project.commit.sha, ref: project.default_branch)
- end
-
let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context "with failed pipeline" do
- let(:status) { "failed" }
+ let_it_be(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: "failed",
+ sha: project.commit.sha, ref: project.default_branch)
+ end
it_behaves_like "triggered #{integration_name} integration"
end
context "with succeeded pipeline" do
- let(:status) { "success" }
+ let_it_be(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: "success",
+ sha: project.commit.sha, ref: project.default_branch)
+ end
context "with default notify_only_broken_pipelines" do
it "does not call #{integration_name} API" do
@@ -308,7 +312,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context "with protected branch" do
- before do
+ before_all do
create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch")
end
@@ -357,7 +361,8 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context 'deployment events' do
- let(:deployment) { create(:deployment) }
+ let_it_be(:deployment) { create(:deployment) }
+
let(:sample_data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) }
it_behaves_like "untriggered #{integration_name} integration"
diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
index 51071ae47c3..ca9122bf61f 100644
--- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
@@ -37,7 +37,7 @@ RSpec.shared_examples 'cluster application core specs' do |application_name|
with_them do
subject { described_class.new(cluster: cluster).helm_command_module }
- let(:cluster) { build(:cluster, helm_major_version: helm_major_version)}
+ let(:cluster) { build(:cluster, helm_major_version: helm_major_version) }
it { is_expected.to eq(expected_helm_command_module) }
end
diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
index 8ff30021d6e..6f104f400bc 100644
--- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
+++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
@@ -89,7 +89,7 @@ RSpec.shared_examples 'StageEventModel' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:user) }
let_it_be(:milestone) { create(:milestone) }
- let_it_be(:issuable_with_assignee) { create(issuable_factory, assignees: [user])}
+ let_it_be(:issuable_with_assignee) { create(issuable_factory, assignees: [user]) }
let_it_be(:record) { create(stage_event_factory, start_event_timestamp: 3.years.ago.to_date, end_event_timestamp: 2.years.ago.to_date) }
let_it_be(:record_with_author) { create(stage_event_factory, author_id: user.id) }
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index f4d5ab3d5c6..f3a12578912 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -75,9 +75,9 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
context 'when attribute is not a counter attribute' do
- it 'delegates to ActiveRecord update!' do
+ it 'raises ArgumentError' do
expect { model.delayed_increment_counter(:unknown_attribute, 10) }
- .to raise_error(ActiveModel::MissingAttributeError)
+ .to raise_error(ArgumentError, 'unknown_attribute is not a counter attribute')
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index a403a27adef..0a07c9d677b 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -125,17 +125,17 @@ RSpec.shared_examples 'model with repository' do
end
describe '#valid_repo?' do
- it { expect(stubbed_container.valid_repo?).to be(false)}
+ it { expect(stubbed_container.valid_repo?).to be(false) }
it { expect(container.valid_repo?).to be(true) }
end
describe '#repository_exists?' do
- it { expect(stubbed_container.repository_exists?).to be(false)}
+ it { expect(stubbed_container.repository_exists?).to be(false) }
it { expect(container.repository_exists?).to be(true) }
end
describe '#repo_exists?' do
- it { expect(stubbed_container.repo_exists?).to be(false)}
+ it { expect(stubbed_container.repo_exists?).to be(false) }
it { expect(container.repo_exists?).to be(true) }
end
diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
index 8ee76efc896..a5970f134d9 100644
--- a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
@@ -77,7 +77,7 @@ RSpec.shared_examples 'a model including Escalatable' do
end
context 'scopes' do
- let(:all_escalatables) { described_class.where(id: [triggered_escalatable, acknowledged_escalatable, ignored_escalatable, resolved_escalatable])}
+ let(:all_escalatables) { described_class.where(id: [triggered_escalatable, acknowledged_escalatable, ignored_escalatable, resolved_escalatable]) }
describe '.order_status' do
subject { all_escalatables.order_status(order) }
diff --git a/spec/support/shared_examples/models/label_note_shared_examples.rb b/spec/support/shared_examples/models/label_note_shared_examples.rb
index 73066fb631a..f61007f57fd 100644
--- a/spec/support/shared_examples/models/label_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/label_note_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_examples 'label note created from events' do
def label_refs(events)
labels = events.map(&:label).compact
- labels.map { |l| l.to_reference}.sort.join(' ')
+ labels.map { |l| l.to_reference }.sort.join(' ')
end
let(:time) { Time.now }
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
index 75eed0203a7..e74aab95e46 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'members notifications' do |entity_type|
end
describe "#after_create" do
- let(:member) { build(:"#{entity_type}_member") }
+ let(:member) { build(:"#{entity_type}_member", "#{entity_type}": create(entity_type.to_s)) }
it "sends email to user" do
expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
@@ -35,7 +35,7 @@ RSpec.shared_examples 'members notifications' do |entity_type|
describe '#after_commit' do
context 'on creation of a member requesting access' do
- let(:member) { build(:"#{entity_type}_member", :access_request) }
+ let(:member) { build(:"#{entity_type}_member", :access_request, "#{entity_type}": create(entity_type.to_s)) }
it "calls NotificationService.new_access_request" do
expect(notification_service).to receive(:new_access_request).with(member)
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index 6b0ae589efb..3d7d97bbeae 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -202,17 +202,17 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
end
else
describe 'group distribution specifics' do
- let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container)}
+ let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container) }
let_it_be(:public_distribution_with_same_codename) { create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename) }
- let_it_be(:public_package_with_same_codename) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename)}
+ let_it_be(:public_package_with_same_codename) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename) }
let_it_be(:public_distribution_with_same_suite) { create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite) }
- let_it_be(:public_package_with_same_suite) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite)}
+ let_it_be(:public_package_with_same_suite) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite) }
- let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container)}
+ let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container) }
let_it_be(:private_distribution_with_same_codename) { create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename) }
- let_it_be(:private_package_with_same_codename) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)}
+ let_it_be(:private_package_with_same_codename) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) }
let_it_be(:private_distribution_with_same_suite) { create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite) }
- let_it_be(:private_package_with_same_suite) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)}
+ let_it_be(:private_package_with_same_suite) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) }
describe '#packages' do
subject { distribution_with_suite.packages }
diff --git a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
index 66cd8d1df12..9093b386a5d 100644
--- a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
@@ -64,7 +64,7 @@ RSpec.shared_examples 'latest successful build for sha or ref' do
context 'with build belonging to a child pipeline' do
let(:child_pipeline) { create_pipeline(project) }
let(:parent_bridge) { create(:ci_bridge, pipeline: pipeline, project: pipeline.project) }
- let!(:pipeline_source) { create(:ci_sources_pipeline, source_job: parent_bridge, pipeline: child_pipeline)}
+ let!(:pipeline_source) { create(:ci_sources_pipeline, source_job: parent_bridge, pipeline: child_pipeline) }
let!(:child_build) { create_build(child_pipeline, 'child-build') }
let(:build_name) { child_build.name }
diff --git a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb
index a41ade2950a..12e865b1312 100644
--- a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'a synthetic note' do |action|
- it_behaves_like 'a system note', exclude_project: true do
+ it_behaves_like 'a system note', exclude_project: true, skip_persistence_check: true do
let(:action) { action }
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index ad0bbc0aeff..b81bd514d0a 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -26,9 +26,6 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
expect(FlushCounterIncrementsWorker)
.to receive(:perform_in)
.with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, project_statistics_name)
- expect(FlushCounterIncrementsWorker)
- .to receive(:perform_in)
- .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, :storage_size)
yield
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 604c57768fe..5f6a10bd754 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -286,67 +286,134 @@ RSpec.shared_examples 'wiki model' do
end
describe '#find_page' do
- before do
- subject.create_page('index page', 'This is an awesome Gollum Wiki')
- end
+ shared_examples 'wiki model #find_page' do
+ before do
+ subject.create_page('index page', 'This is an awesome Gollum Wiki')
+ end
- it 'returns the latest version of the page if it exists' do
- page = subject.find_page('index page')
+ it 'returns the latest version of the page if it exists' do
+ page = subject.find_page('index page')
- expect(page.title).to eq('index page')
- end
+ expect(page.title).to eq('index page')
+ end
- it 'returns nil if the page or version does not exist' do
- expect(subject.find_page('non-existent')).to be_nil
- expect(subject.find_page('index page', 'non-existent')).to be_nil
- end
+ it 'returns nil if the page or version does not exist' do
+ expect(subject.find_page('non-existent')).to be_nil
+ expect(subject.find_page('index page', 'non-existent')).to be_nil
+ end
- it 'can find a page by slug' do
- page = subject.find_page('index-page')
+ it 'can find a page by slug' do
+ page = subject.find_page('index-page')
- expect(page.title).to eq('index page')
- end
+ expect(page.title).to eq('index page')
+ end
- it 'returns a WikiPage instance' do
- page = subject.find_page('index page')
+ it 'returns a WikiPage instance' do
+ page = subject.find_page('index page')
- expect(page).to be_a WikiPage
- end
+ expect(page).to be_a WikiPage
+ end
- context 'pages with multibyte-character title' do
- before do
- subject.create_page('autre pagé', "C'est un génial Gollum Wiki")
+ context 'pages with multibyte-character title' do
+ before do
+ subject.create_page('autre pagé', "C'est un génial Gollum Wiki")
+ end
+
+ it 'can find a page by slug' do
+ page = subject.find_page('autre pagé')
+
+ expect(page.title).to eq('autre pagé')
+ end
end
- it 'can find a page by slug' do
- page = subject.find_page('autre pagé')
+ context 'pages with invalidly-encoded content' do
+ before do
+ subject.create_page('encoding is fun', "f\xFCr".b)
+ end
+
+ it 'can find the page' do
+ page = subject.find_page('encoding is fun')
+
+ expect(page.content).to eq('fr')
+ end
+ end
+
+ context 'pages with different file extensions' do
+ where(:extension, :path, :title) do
+ [
+ [:md, "wiki-markdown.md", "wiki markdown"],
+ [:markdown, "wiki-markdown-2.md", "wiki markdown 2"],
+ [:rdoc, "wiki-rdoc.rdoc", "wiki rdoc"],
+ [:asciidoc, "wiki-asciidoc.asciidoc", "wiki asciidoc"],
+ [:adoc, "wiki-asciidoc-2.adoc", "wiki asciidoc 2"],
+ [:org, "wiki-org.org", "wiki org"],
+ [:textile, "wiki-textile.textile", "wiki textile"],
+ [:creole, "wiki-creole.creole", "wiki creole"],
+ [:rest, "wiki-rest.rest", "wiki rest"],
+ [:rst, "wiki-rest-2.rst", "wiki rest 2"],
+ [:mediawiki, "wiki-mediawiki.mediawiki", "wiki mediawiki"],
+ [:wiki, "wiki-mediawiki-2.wiki", "wiki mediawiki 2"],
+ [:pod, "wiki-pod.pod", "wiki pod"],
+ [:text, "wiki-text.txt", "wiki text"]
+ ]
+ end
- expect(page.title).to eq('autre pagé')
+ with_them do
+ before do
+ wiki.repository.create_file(
+ user, path, "content of wiki file",
+ branch_name: wiki.default_branch,
+ message: "created page #{path}",
+ author_email: user.email,
+ author_name: user.name
+ )
+ end
+
+ it "can find page with #{params[:extension]} extension" do
+ page = subject.find_page(title)
+
+ expect(page.content).to eq("content of wiki file")
+ end
+ end
end
end
- context 'pages with invalidly-encoded content' do
+ context 'find page with legacy wiki service' do
before do
- subject.create_page('encoding is fun', "f\xFCr".b)
+ stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
end
- it 'can find the page' do
- page = subject.find_page('encoding is fun')
+ it_behaves_like 'wiki model #find_page'
+ end
- expect(page.content).to eq('fr')
- end
+ context 'find page with normal repository RPCs' do
+ it_behaves_like 'wiki model #find_page'
end
end
describe '#find_sidebar' do
- before do
- subject.create_page(described_class::SIDEBAR, 'This is an awesome Sidebar')
+ shared_examples 'wiki model #find_sidebar' do
+ before do
+ subject.create_page(described_class::SIDEBAR, 'This is an awesome Sidebar')
+ end
+
+ it 'finds the page defined as _sidebar' do
+ page = subject.find_sidebar
+
+ expect(page.content).to eq('This is an awesome Sidebar')
+ end
end
- it 'finds the page defined as _sidebar' do
- page = subject.find_sidebar
+ context 'find sidebar with legacy wiki service' do
+ before do
+ stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
+ end
- expect(page.content).to eq('This is an awesome Sidebar')
+ it_behaves_like 'wiki model #find_sidebar'
+ end
+
+ context 'find sidebar with normal repository RPCs' do
+ it_behaves_like 'wiki model #find_sidebar'
end
end
@@ -450,9 +517,7 @@ RSpec.shared_examples 'wiki model' do
expect(subject.error_message).to match(/Duplicate page:/)
end
- end
- it_behaves_like 'create_page tests' do
it 'returns false if a page exists already in the repository', :aggregate_failures do
subject.create_page('test page', 'content')
@@ -540,6 +605,16 @@ RSpec.shared_examples 'wiki model' do
end
end
end
+
+ it_behaves_like 'create_page tests'
+
+ context 'create page with legacy find_page wiki service' do
+ it_behaves_like 'create_page tests' do
+ before do
+ stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
+ end
+ end
+ end
end
describe '#update_page' do
@@ -636,6 +711,17 @@ RSpec.shared_examples 'wiki model' do
include_context 'extended examples'
end
+ context 'update page with legacy find_page wiki service' do
+ it_behaves_like 'update_page tests' do
+ before do
+ stub_feature_flags(wiki_find_page_with_normal_repository_rpcs: false)
+ end
+
+ include_context 'common examples'
+ include_context 'extended examples'
+ end
+ end
+
context 'when format is invalid' do
let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') }
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 807295f8442..4afed5139d8 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -265,14 +265,6 @@ RSpec.shared_examples 'namespace traversal scopes' do
describe '.self_and_descendants' do
include_examples '.self_and_descendants'
-
- context 'with traversal_ids_btree feature flag disabled' do
- before do
- stub_feature_flags(traversal_ids_btree: false)
- end
-
- include_examples '.self_and_descendants'
- end
end
shared_examples '.self_and_descendant_ids' do
@@ -308,14 +300,6 @@ RSpec.shared_examples 'namespace traversal scopes' do
describe '.self_and_descendant_ids' do
include_examples '.self_and_descendant_ids'
-
- context 'with traversal_ids_btree feature flag disabled' do
- before do
- stub_feature_flags(traversal_ids_btree: false)
- end
-
- include_examples '.self_and_descendant_ids'
- end
end
shared_examples '.self_and_hierarchy' do
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index c4083df47e2..cfcc3615e13 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -107,70 +107,88 @@ RSpec.shared_examples 'deploy token does not get confused with user' do
end
RSpec.shared_examples 'project policies as guest' do
- context 'abilities for public projects' do
- let(:project) { public_project }
- let(:current_user) { guest }
-
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*public_permissions)
- expect_disallowed(*developer_permissions)
- expect_disallowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
- end
+ let(:reporter_public_build_permissions) do
+ reporter_permissions - [:read_build, :read_pipeline]
end
- context 'abilities for non-public projects' do
- let(:project) { private_project }
- let(:current_user) { guest }
+ context 'as a direct project member' do
+ context 'abilities for public projects' do
+ let(:project) { public_project }
+ let(:current_user) { guest }
- let(:reporter_public_build_permissions) do
- reporter_permissions - [:read_build, :read_pipeline]
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*public_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
end
- it do
- expect_allowed(*guest_permissions)
- expect_disallowed(*reporter_public_build_permissions)
- expect_disallowed(*team_member_reporter_permissions)
- expect_disallowed(*developer_permissions)
- expect_disallowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
- end
+ context 'abilities for non-public projects' do
+ let(:project) { private_project }
+ let(:current_user) { guest }
- it_behaves_like 'deploy token does not get confused with user' do
- let(:user_id) { guest.id }
- end
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_disallowed(*reporter_public_build_permissions)
+ expect_disallowed(*team_member_reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { guest_permissions }
- end
+ it_behaves_like 'deploy token does not get confused with user' do
+ let(:user_id) { guest.id }
+ end
- context 'public builds enabled' do
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(:read_build, :read_pipeline)
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { guest_permissions }
end
- end
- context 'when public builds disabled' do
- before do
- project.update!(public_builds: false)
+ context 'public builds enabled' do
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_allowed(:read_build, :read_pipeline)
+ end
end
- it do
- expect_allowed(*guest_permissions)
- expect_disallowed(:read_build, :read_pipeline)
+ context 'when public builds disabled' do
+ before do
+ project.update!(public_builds: false)
+ end
+
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_disallowed(:read_build, :read_pipeline)
+ end
end
- end
- context 'when builds are disabled' do
- before do
- project.project_feature.update!(builds_access_level: ProjectFeature::DISABLED)
+ context 'when builds are disabled' do
+ before do
+ project.project_feature.update!(builds_access_level: ProjectFeature::DISABLED)
+ end
+
+ specify do
+ expect_disallowed(:read_build)
+ expect_allowed(:read_pipeline)
+ end
end
+ end
+ end
- it do
- expect_disallowed(:read_build)
- expect_allowed(:read_pipeline)
+ context 'as an inherited member from the group' do
+ context 'abilities for private projects' do
+ let(:project) { private_project_in_group }
+ let(:current_user) { inherited_guest }
+
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_disallowed(*reporter_public_build_permissions)
+ expect_disallowed(*team_member_reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
end
end
end
@@ -181,7 +199,7 @@ RSpec.shared_examples 'project policies as reporter' do
let(:project) { private_project }
let(:current_user) { reporter }
- it do
+ specify do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
@@ -198,6 +216,22 @@ RSpec.shared_examples 'project policies as reporter' do
let(:regular_abilities) { reporter_permissions }
end
end
+
+ context 'as an inherited member from the group' do
+ context 'abilities for private projects' do
+ let(:project) { private_project_in_group }
+ let(:current_user) { inherited_reporter }
+
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*team_member_reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+ end
+ end
end
RSpec.shared_examples 'project policies as developer' do
@@ -205,7 +239,7 @@ RSpec.shared_examples 'project policies as developer' do
let(:project) { private_project }
let(:current_user) { developer }
- it do
+ specify do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
@@ -222,6 +256,22 @@ RSpec.shared_examples 'project policies as developer' do
let(:regular_abilities) { developer_permissions }
end
end
+
+ context 'as an inherited member from the group' do
+ context 'abilities for private projects' do
+ let(:project) { private_project_in_group }
+ let(:current_user) { inherited_developer }
+
+ specify do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*team_member_reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+ end
+ end
end
RSpec.shared_examples 'project policies as maintainer' do
diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
new file mode 100644
index 00000000000..9c2d30a9c8c
--- /dev/null
+++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
@@ -0,0 +1,263 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling invalid params' do |service_response_extra: {}, supports_caching: false|
+ context 'when no params are specified' do
+ let(:params) { {} }
+
+ it_behaves_like 'not removing anything',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching
+ end
+
+ context 'with invalid regular expressions' do
+ shared_examples 'handling an invalid regex' do
+ it 'keeps all tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .not_to receive(:new)
+ expect_no_caching unless supports_caching
+
+ subject
+ end
+
+ it { is_expected.to eq(status: :error, message: 'invalid regex') }
+
+ it 'calls error tracking service' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when name_regex_delete is invalid' do
+ let(:params) { { 'name_regex_delete' => '*test*' } }
+
+ it_behaves_like 'handling an invalid regex'
+ end
+
+ context 'when name_regex is invalid' do
+ let(:params) { { 'name_regex' => '*test*' } }
+
+ it_behaves_like 'handling an invalid regex'
+ end
+
+ context 'when name_regex_keep is invalid' do
+ let(:params) { { 'name_regex_keep' => '*test*' } }
+
+ it_behaves_like 'handling an invalid regex'
+ end
+ end
+end
+
+RSpec.shared_examples 'when regex matching everything is specified' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ { 'name_regex_delete' => '.*' }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+
+ context 'with deprecated name_regex param' do
+ let(:params) do
+ { 'name_regex' => '.*' }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+ end
+end
+
+RSpec.shared_examples 'when delete regex matching specific tags is used' do
+ |service_response_extra: {}, supports_caching: false|
+ let(:params) do
+ { 'name_regex_delete' => 'C|D' }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[C D]]
+end
+
+RSpec.shared_examples 'when delete regex matching specific tags is used with overriding allow regex' do
+ |service_response_extra: {}, supports_caching: false|
+ let(:params) do
+ {
+ 'name_regex_delete' => 'C|D',
+ 'name_regex_keep' => 'C'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[D]]
+
+ context 'with name_regex_delete overriding deprecated name_regex' do
+ let(:params) do
+ {
+ 'name_regex' => 'C|D',
+ 'name_regex_delete' => 'D'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: [%w[D]]
+ end
+end
+
+RSpec.shared_examples 'with allow regex value' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'name_regex_keep' => 'B.*'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when keeping only N tags' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex' => 'A|B.*|C',
+ 'keep_n' => 1
+ }
+ end
+
+ it 'sorts tags by date' do
+ delete_expectations.each { |expectation| expect_delete(expectation) }
+ expect_no_caching unless supports_caching
+
+ expect(service).to receive(:order_by_date_desc).at_least(:once).and_call_original
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+end
+
+RSpec.shared_examples 'when not keeping N tags' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ { 'name_regex' => 'A|B.*|C' }
+ end
+
+ it 'does not sort tags by date' do
+ delete_expectations.each { |expectation| expect_delete(expectation) }
+ expect_no_caching unless supports_caching
+
+ expect(service).not_to receive(:order_by_date_desc)
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+end
+
+RSpec.shared_examples 'when removing keeping only 3' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'keep_n' => 3 }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when removing older than 1 day' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'older_than' => '1 day'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when combining all parameters' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day'
+ }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
+RSpec.shared_examples 'when running a container_expiration_policy' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ let(:user) { nil }
+
+ context 'with valid container_expiration_policy param' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day',
+ 'container_expiration_policy' => true
+ }
+ end
+
+ it 'removes the expected tags' do
+ delete_expectations.each { |expectation| expect_delete(expectation, container_expiration_policy: true) }
+ expect_no_caching unless supports_caching
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+ end
+
+ context 'without container_expiration_policy param' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day'
+ }
+ end
+
+ it 'fails' do
+ is_expected.to eq(status: :error, message: 'access denied')
+ end
+ end
+end
+
+RSpec.shared_examples 'not removing anything' do |service_response_extra: {}, supports_caching: false|
+ it 'does not remove anything' do
+ expect(Projects::ContainerRepository::DeleteTagsService).not_to receive(:new)
+ expect_no_caching unless supports_caching
+
+ is_expected.to eq(expected_service_response(deleted: []).merge(service_response_extra))
+ end
+end
+
+RSpec.shared_examples 'removing the expected tags' do
+ |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ it 'removes the expected tags' do
+ delete_expectations.each { |expectation| expect_delete(expectation) }
+ expect_no_caching unless supports_caching
+
+ is_expected.to eq(expected_service_response(deleted: delete_expectations.flatten).merge(service_response_extra))
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/incident/timeline_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/incident/timeline_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..ae7e511a739
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/incident/timeline_quick_action_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'timeline quick action' do
+ describe '/timeline' do
+ context 'with valid args' do
+ where(:timeline_text, :date_time_arg) do
+ [
+ ['timeline comment', '2022-09-09 09:30'],
+ ['new timeline comment', '09:30'],
+ ['another timeline comment', ' 2022-09-09 09:15']
+ ]
+ end
+
+ with_them do
+ it 'adds a timeline event' do
+ add_note("/timeline #{timeline_text} | #{date_time_arg}")
+
+ expect(page).to have_content('Timeline event added successfully.')
+ expect(issue.incident_management_timeline_events.first.note).to eq(timeline_text)
+ expect(issue.incident_management_timeline_events.first.occurred_at).to eq(DateTime.parse(date_time_arg))
+ end
+ end
+
+ it 'adds a timeline event when no date is passed' do
+ freeze_time do
+ add_note('/timeline timeline event with not date')
+
+ expect(page).to have_content('Timeline event added successfully.')
+ expect(issue.incident_management_timeline_events.first.note).to eq('timeline event with not date')
+ expect(issue.incident_management_timeline_events.first.occurred_at).to eq(DateTime
+ .current.strftime("%Y-%m-%d %H:%M:00 UTC"))
+ end
+ end
+
+ it 'adds a timeline event when only date is passed' do
+ freeze_time do
+ add_note('/timeline timeline event with not date | 2022-10-11')
+
+ expect(page).to have_content('Timeline event added successfully.')
+ expect(issue.incident_management_timeline_events.first.note).to eq('timeline event with not date')
+ expect(issue.incident_management_timeline_events.first.occurred_at).to eq(DateTime
+ .current.strftime("%Y-%m-%d %H:%M:00 UTC"))
+ end
+ end
+ end
+
+ context 'with invalid args' do
+ where(:timeline_text, :date_time_arg) do
+ [
+ ['timeline comment', '2022-13-13 09:30'],
+ ['timeline comment 2', '2022-09-06 24:30']
+ ]
+ end
+
+ with_them do
+ it 'does not add a timeline event' do
+ add_note("/timeline #{timeline_text} | #{date_time_arg}")
+
+ expect(page).to have_content('Failed to apply commands.')
+ expect(issue.incident_management_timeline_events.length).to eq(0)
+ end
+ end
+ end
+
+ context 'when create service fails' do
+ before do
+ allow_next_instance_of(::IncidentManagement::TimelineEvents::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(
+ ServiceResponse.error(payload: { timeline_event: nil }, message: 'Some error')
+ )
+ end
+ end
+
+ it 'does not add a timeline event' do
+ add_note('/timeline text | 2022-09-10 09:30')
+
+ expect(page).to have_content('Something went wrong while adding timeline event.')
+ expect(issue.incident_management_timeline_events.length).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index f414500f202..18304951e41 100644
--- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable time tracker' do |issuable_type|
+ let_it_be(:time_tracker_selector) { '[data-testid="time-tracker"]' }
+
before do
project.add_maintainer(maintainer)
gitlab_sign_in(maintainer)
@@ -12,6 +14,14 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
wait_for_requests
end
+ def open_time_tracking_report
+ page.within time_tracker_selector do
+ click_link 'Time tracking report'
+
+ wait_for_requests
+ end
+ end
+
it 'renders the sidebar component empty state' do
page.within '[data-testid="noTrackingPane"]' do
expect(page).to have_content 'No estimate or time spent'
@@ -50,7 +60,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
submit_time('/estimate 3w 1d 1h')
submit_time('/remove_estimate')
- page.within '.time-tracking-component-wrap' do
+ page.within time_tracker_selector do
expect(page).to have_content 'No estimate or time spent'
end
end
@@ -59,13 +69,13 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
submit_time('/spend 3w 1d 1h')
submit_time('/remove_time_spent')
- page.within '.time-tracking-component-wrap' do
+ page.within time_tracker_selector do
expect(page).to have_content 'No estimate or time spent'
end
end
it 'shows the help state when icon is clicked' do
- page.within '.time-tracking-component-wrap' do
+ page.within time_tracker_selector do
find('[data-testid="helpButton"]').click
expect(page).to have_content 'Track time with quick actions'
expect(page).to have_content 'Learn more'
@@ -78,11 +88,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
wait_for_requests
- page.within '.time-tracking-component-wrap' do
- click_link 'Time tracking report'
-
- wait_for_requests
- end
+ open_time_tracking_report
page.within '#time-tracking-report' do
expect(find('tbody')).to have_content maintainer.name
@@ -90,8 +96,36 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
end
end
+ it 'removes time log when delete is clicked in time tracking report' do
+ submit_time('/estimate 1w')
+ submit_time('/spend 1d')
+ submit_time('/spend 3d')
+
+ wait_for_requests
+
+ open_time_tracking_report
+
+ page.within '#time-tracking-report tbody tr:nth-child(2)' do
+ click_button test_id: 'deleteButton'
+
+ wait_for_requests
+ end
+
+ # Assert that 2nd row was removed
+ expect(all('#time-tracking-report tbody tr').length).to eq(1)
+ expect(find('#time-tracking-report tbody')).not_to have_content('3d')
+
+ # Assert that summary line was updated
+ expect(find('#time-tracking-report tfoot')).to have_content('1d', exact: true)
+
+ # Assert that the time tracking widget was reactively updated
+ page.within '[data-testid="timeTrackingComparisonPane"]' do
+ expect(page).to have_content '1d'
+ end
+ end
+
it 'hides the help state when close icon is clicked' do
- page.within '.time-tracking-component-wrap' do
+ page.within time_tracker_selector do
find('[data-testid="helpButton"]').click
find('[data-testid="closeHelpButton"]').click
@@ -101,7 +135,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
end
it 'displays the correct help url' do
- page.within '.time-tracking-component-wrap' do
+ page.within time_tracker_selector do
find('[data-testid="helpButton"]').click
expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md')
diff --git a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
index 2258bdd2c79..92705fc1b4d 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb
@@ -75,7 +75,16 @@ RSpec.shared_examples 'rebase quick action' do
end
context 'when the merge request branch is protected from force push' do
- let!(:protected_branch) { create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) }
+ let!(:protected_branch) do
+ ProtectedBranches::CreateService.new(
+ project,
+ user,
+ name: merge_request.source_branch,
+ allow_force_push: false,
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }],
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]
+ ).execute
+ end
it 'does not rebase the MR' do
add_note("/rebase")
diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
index 6cd871d354c..017e6274cb0 100644
--- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
@@ -108,18 +108,31 @@ RSpec.shared_examples 'PUT resource access tokens available' do
expect(resource.reload.bots).not_to include(bot_user)
end
- it 'converts issuables of the bot user to ghost user' do
- issue = create(:issue, author: bot_user)
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'creates GhostUserMigration records to handle migration in a worker' do
+ expect { subject }.to(
+ change { Users::GhostUserMigration.count }.from(0).to(1))
+ end
+ end
- subject
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
- expect(issue.reload.author.ghost?).to be true
- end
+ it 'converts issuables of the bot user to ghost user' do
+ issue = create(:issue, author: bot_user)
- it 'deletes project bot user' do
- subject
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
- expect(User.exists?(bot_user.id)).to be_falsy
+ it 'deletes project bot user' do
+ subject
+
+ expect(User.exists?(bot_user.id)).to be_falsy
+ end
end
context 'when unsuccessful' do
diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
index dc2c4f890b1..6a77de4266f 100644
--- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
@@ -108,6 +108,7 @@ RSpec.shared_examples 'process Composer api request' do |user_type, status, add_
end
it_behaves_like 'returning response status', status
+ it_behaves_like 'bumping the package last downloaded at field' if status == :success
end
end
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index bb2f8965294..629d93676eb 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -355,7 +355,7 @@ RSpec.shared_examples 'recipe download_urls' do
it 'returns the download_urls for the recipe files' do
expected_response = {
- 'conanfile.py' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanfile.py' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
}
@@ -372,7 +372,7 @@ RSpec.shared_examples 'package download_urls' do
it 'returns the download_urls for the package files' do
expected_response = {
- 'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
}
@@ -412,7 +412,7 @@ RSpec.shared_examples 'recipe snapshot endpoint' do
conan_manifest_file = package.package_files.find_by(file_name: 'conanmanifest.txt')
expected_response = {
- 'conanfile.py' => conan_file_file.file_md5,
+ 'conanfile.py' => conan_file_file.file_md5,
'conanmanifest.txt' => conan_manifest_file.file_md5
}
@@ -435,7 +435,7 @@ RSpec.shared_examples 'package snapshot endpoint' do
context 'with existing package' do
it 'returns a hash of md5 values for the files' do
expected_response = {
- 'conaninfo.txt' => "12345abcde",
+ 'conaninfo.txt' => "12345abcde",
'conanmanifest.txt' => "12345abcde",
'conan_package.tgz' => "12345abcde"
}
@@ -486,7 +486,7 @@ RSpec.shared_examples 'recipe upload_urls endpoint' do
subject
expected_response = {
- 'conanfile.py': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanfile.py': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
}
@@ -505,7 +505,7 @@ RSpec.shared_examples 'recipe upload_urls endpoint' do
expected_response = {
'conan_sources.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz",
- 'conan_export.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
+ 'conan_export.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
}
@@ -547,7 +547,7 @@ RSpec.shared_examples 'package upload_urls endpoint' do
it 'returns a set of upload urls for the files requested' do
expected_response = {
- 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
'conan_package.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
}
@@ -631,6 +631,7 @@ RSpec.shared_examples 'a public project with packages' do
end
it_behaves_like 'allows download with no token'
+ it_behaves_like 'bumping the package last downloaded at field'
it 'returns the file' do
subject
@@ -647,6 +648,7 @@ RSpec.shared_examples 'an internal project with packages' do
end
it_behaves_like 'denies download with no token'
+ it_behaves_like 'bumping the package last downloaded at field'
it 'returns the file' do
subject
@@ -662,6 +664,7 @@ RSpec.shared_examples 'a private project with packages' do
end
it_behaves_like 'denies download with no token'
+ it_behaves_like 'bumping the package last downloaded at field'
it 'returns the file' do
subject
diff --git a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb
new file mode 100644
index 00000000000..22805cf7aed
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Requires `query(params)` , `user`, `issuable_data` and `issuable` bindings
+RSpec.shared_examples 'query with a search term' do
+ it 'returns only matching issuables' do
+ filter_params = { search: 'bar', in: [:DESCRIPTION] }
+ graphql_query = query(filter_params)
+
+ post_graphql(graphql_query, current_user: user)
+ ids = graphql_dig_at(issuable_data, :node, :id)
+
+ expect(ids).to contain_exactly(issuable.to_global_id.to_s)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index 9f7ec6e90e9..1b609915f32 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -9,9 +9,10 @@ RSpec.shared_examples 'group and project packages query' do
let_it_be(:composer_package) { create(:composer_package, project: project2, name: 'dab', version: '4.0.0', created_at: 3.days.ago) }
let_it_be(:debian_package) { create(:debian_package, project: project2, name: 'aab', version: '5.0.0', created_at: 2.days.ago) }
let_it_be(:composer_metadatum) do
- create(:composer_metadatum, package: composer_package,
- target_sha: 'afdeh',
- composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
+ create(:composer_metadatum,
+ package: composer_package,
+ target_sha: 'afdeh',
+ composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
end
let(:package_names) { graphql_data_at(resource_type, :packages, :nodes, :name) }
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
index acbcf4f7f3d..06ed0448b50 100644
--- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
@@ -191,14 +191,15 @@ RSpec.shared_examples 'process helm download content request' do |user_type, sta
end
end
- it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package'
-
it 'returns expected status and a valid package archive' do
subject
expect(response).to have_gitlab_http_status(status)
expect(response.media_type).to eq('application/octet-stream')
end
+
+ it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
end
@@ -278,7 +279,6 @@ RSpec.shared_examples 'handling helm chart index requests' do
end
it_behaves_like 'deploy token for package GET requests'
-
it_behaves_like 'rejects helm access with unknown project id' do
subject { get api(url) }
end
diff --git a/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb b/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb
index 971b21b5b32..8c4ff120471 100644
--- a/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
def get_issue
- json_response.is_a?(Array) ? json_response.detect {|issue| issue['id'] == target_issue.id} : json_response
+ json_response.is_a?(Array) ? json_response.detect { |issue| issue['id'] == target_issue.id } : json_response
end
RSpec.shared_examples 'accessible merge requests count' do
diff --git a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb
index 02e50b789cc..41d21490343 100644
--- a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb
@@ -9,6 +9,6 @@ RSpec.shared_examples 'fetches labels' do
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(expected_labels.size)
- expect(json_response.map {|r| r['name'] }).to match_array(expected_labels)
+ expect(json_response.map { |r| r['name'] }).to match_array(expected_labels)
end
end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index 6568d51b90e..fdd55893deb 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -293,6 +293,8 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
+
it 'returns a valid package archive' do
subject
@@ -315,6 +317,8 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
end
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_symbol_package'
+
+ it_behaves_like 'bumping the package last downloaded at field'
end
context 'with lower case package name' do
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index eb650b7a09f..860cb1b1d86 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -165,3 +165,10 @@ RSpec.shared_examples 'not a package tracking event' do
expect_no_snowplow_event
end
end
+
+RSpec.shared_examples 'bumping the package last downloaded at field' do
+ it 'bumps last_downloaded_at' do
+ expect { subject }
+ .to change { package.reload.last_downloaded_at }.from(nil).to(instance_of(ActiveSupport::TimeWithZone))
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index ba8311bf0be..f411b5699a9 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -167,6 +167,7 @@ RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member
it_behaves_like 'returning response status', status
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
end
diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
index 3ca2b9fa6de..2d036cb2aa3 100644
--- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
@@ -70,7 +70,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
get_container_repository_storage_moves
- json_ids = json_response.map {|storage_move| storage_move['id'] }
+ json_ids = json_response.map { |storage_move| storage_move['id'] }
expect(json_ids).to eq([
storage_move.id,
storage_move_middle.id,
diff --git a/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb
new file mode 100644
index 00000000000..c1850a0d0c9
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name|
+ let(:base_path) { "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}" }
+
+ describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do
+ let!(:event) { create_event }
+
+ it "returns an array of resource state events" do
+ url = "#{base_path}/resource_state_events"
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(event.id)
+ expect(json_response.first['state']).to eq(event.state.to_s)
+ end
+
+ it "returns a 404 error when eventable id not found" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ private_user = create(:user)
+
+ get api("#{base_path}/resource_state_events", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do
+ let!(:event) { create_event }
+
+ it "returns a resource state event by id" do
+ get api("#{base_path}/resource_state_events/#{event.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(event.id)
+ expect(json_response['state']).to eq(event.state.to_s)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ private_user = create(:user)
+
+ get api("#{base_path}/resource_state_events/#{event.id}", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it "returns a 404 error if resource state event not found" do
+ get api("#{base_path}/resource_state_events/#{non_existing_record_id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'pagination' do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/220192
+ it 'returns the second page' do
+ create_event
+ event2 = create_event
+
+ get api("#{base_path}/resource_state_events?page=2&per_page=1", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq '2'
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['id']).to eq(event2.id)
+ end
+ end
+
+ def create_event(state: :opened)
+ create(:resource_state_event, eventable.class.name.underscore => eventable, state: state)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
index abdb468353a..f075927e7bf 100644
--- a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
@@ -203,5 +203,6 @@ RSpec.shared_examples 'Rubygems gem download' do |user_type, status, add_member
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
+ it_behaves_like 'bumping the package last downloaded at field'
end
end
diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
index 2b72c69cb37..1b92eb56f54 100644
--- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
@@ -133,7 +133,7 @@ RSpec.shared_examples 'snippet file updates' do
context 'when save fails due to a repository commit error' do
before do
allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
+ allow(instance).to receive(:commit_files).and_raise(Gitlab::Git::CommitError)
end
update_snippet(params: { files: [create_action] })
diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
index 8f852d42c2c..642930dd982 100644
--- a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
@@ -11,6 +11,7 @@ RSpec.shared_examples 'applications controller - GET #show' do
context 'when application is viewed after being created' do
before do
create_application
+ stub_feature_flags(hash_oauth_secrets: false)
end
it 'sets `@created` instance variable to `true`' do
@@ -21,6 +22,10 @@ RSpec.shared_examples 'applications controller - GET #show' do
end
context 'when application is reviewed' do
+ before do
+ stub_feature_flags(hash_oauth_secrets: false)
+ end
+
it 'sets `@created` instance variable to `false`' do
get show_path
@@ -32,6 +37,7 @@ end
RSpec.shared_examples 'applications controller - POST #create' do
it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do
+ stub_feature_flags(hash_oauth_secrets: false)
create_application
expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true)
diff --git a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
index 294ceffd77b..83be0cc1fe3 100644
--- a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
+++ b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
@@ -49,7 +49,7 @@ RSpec.shared_examples 'LFS http 404 response' do
end
RSpec.shared_examples 'LFS http expected response code and message' do
- let(:response_code) { }
+ let(:response_code) {}
let(:response_headers) { {} }
let(:content_type) { LfsRequest::CONTENT_TYPE }
let(:message) {}
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb
new file mode 100644
index 00000000000..d49fe517c60
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires feature flag `incubation_5mp_google_cloud` enabled' do
+ context 'when feature flag is disabled' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(incubation_5mp_google_cloud: false)
+ end
+
+ it 'renders not found' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb
new file mode 100644
index 00000000000..4c616b59be0
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires `admin_project_google_cloud` role' do
+ shared_examples 'returns not_found' do
+ it 'returns not found' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'redirects to authorize url' do
+ it 'redirects to authorize url' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+
+ context 'when requested by users with different roles' do
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
+
+ context 'for unauthorized users' do
+ include_examples 'returns not_found' do
+ let(:user) { guest }
+ end
+
+ include_examples 'returns not_found' do
+ let(:user) { developer }
+ end
+ end
+
+ context 'for authorized users' do
+ include_examples 'redirects to authorize url' do
+ let(:user) { maintainer }
+ end
+
+ include_examples 'redirects to authorize url' do
+ let(:user) { project.owner }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb
new file mode 100644
index 00000000000..63f6cffb3a0
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires valid Google OAuth2 configuration' do
+ context 'when GitLab instance does not have valid Google OAuth2 configuration ' do
+ before do
+ project.add_maintainer(user)
+ unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
+ .new('', '')
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
+ .with('google_oauth2')
+ .and_return(unconfigured_google_oauth2)
+ end
+
+ it 'renders forbidden' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb
new file mode 100644
index 00000000000..379327be0db
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires valid Google Oauth2 token' do
+ context 'when a valid Google OAuth2 token does not exist' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'triggers Google OAuth2 flow on request' do
+ subject
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+
+ context 'and a valid Google OAuth2 token gets created' do
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ allow(client).to receive(:list_projects).and_return(mock_gcp_projects) if mock_gcp_projects
+ end
+
+ allow_next_instance_of(BranchesFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches) if mock_branches
+ end
+
+ allow_next_instance_of(TagsFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches) if mock_branches
+ end
+ end
+
+ it 'renders template as expected' do
+ if renders_template
+ subject
+ expect(response).to render_template(renders_template)
+ end
+ end
+
+ it 'redirects as expected' do
+ if redirects_to
+ subject
+ expect(response).to redirect_to(redirects_to)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index d4417b23a5f..11759b6671f 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -518,7 +518,7 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
context 'when the request is to the api internal endpoints' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get '/api/v4/internal/check', params: { secret_token: Gitlab::Shell.secret_token }
+ get '/api/v4/internal/check', headers: GitlabShellHelpers.gitlab_shell_internal_api_request_header
expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/support/shared_examples/routing/resource_routing_shared_examples.rb b/spec/support/shared_examples/routing/resource_routing_shared_examples.rb
index b98901a57ea..d7a674f3522 100644
--- a/spec/support/shared_examples/routing/resource_routing_shared_examples.rb
+++ b/spec/support/shared_examples/routing/resource_routing_shared_examples.rb
@@ -43,12 +43,12 @@ RSpec.shared_examples 'resource routing' do
let(:default_actions) do
{
- index: [:get, ''],
- show: [:get, '/:id'],
- new: [:get, '/new'],
- create: [:post, ''],
- edit: [:get, '/:id/edit'],
- update: [:put, '/:id'],
+ index: [:get, ''],
+ show: [:get, '/:id'],
+ new: [:get, '/new'],
+ create: [:post, ''],
+ edit: [:get, '/:id/edit'],
+ update: [:put, '/:id'],
destroy: [:delete, '/:id']
}
end
diff --git a/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb b/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb
index 9289934677e..64f237f0d4d 100644
--- a/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb
+++ b/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb
@@ -6,9 +6,9 @@ RSpec.shared_examples 'wiki routing' do
let(:actions) { %i[show new create edit update destroy] }
let(:additional_actions) do
{
- pages: [:get, '/pages'],
- history: [:get, '/:id/history'],
- git_access: [:get, '/git_access'],
+ pages: [:get, '/pages'],
+ history: [:get, '/:id/history'],
+ git_access: [:get, '/git_access'],
preview_markdown: [:post, '/:id/preview_markdown']
}
end
diff --git a/spec/support/shared_examples/security_training_providers_importer.rb b/spec/support/shared_examples/security_training_providers_importer.rb
new file mode 100644
index 00000000000..568e3e1a4f2
--- /dev/null
+++ b/spec/support/shared_examples/security_training_providers_importer.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'security training providers importer' do
+ let(:security_training_providers) do
+ Class.new(ApplicationRecord) do
+ self.table_name = 'security_training_providers'
+ end
+ end
+
+ it 'upserts security training providers' do
+ expect { 2.times { subject } }.to change(security_training_providers, :count).from(0).to(2)
+ expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior'])
+ end
+end
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
index 6d59943d91c..9eaad541df7 100644
--- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -9,7 +9,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
# See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317
- relax_count = 1
+ # See also: https://gitlab.com/gitlab-org/gitlab/-/issues/373151
+ relax_count = 4
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count)
end
@@ -23,7 +24,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
# See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317
- relax_count = 1
+ # See also: https://gitlab.com/gitlab-org/gitlab/-/issues/373151
+ relax_count = 5
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count)
end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
index 6cae7d8e00f..0db9519f760 100644
--- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
@@ -23,12 +23,10 @@ RSpec.shared_examples 'creates an alert management alert or errors' do
end
context 'and fails to save' do
- let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] }, '[]': [] )}
-
before do
- allow(service).to receive(:alert).and_call_original
- allow(service).to receive_message_chain(:alert, :save).and_return(false)
- allow(service).to receive_message_chain(:alert, :errors).and_return(errors)
+ allow(AlertManagement::Alert).to receive(:new).and_wrap_original do |m, **args|
+ m.call(**args, hosts: ['a' * 256]) # hosts should be 255
+ end
end
it_behaves_like 'alerts service responds with an error', :bad_request
diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
index a46c2f0ac5c..162be24fe8f 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
@@ -3,8 +3,8 @@
RSpec.shared_examples 'issues move service' do |group|
shared_examples 'updating timestamps' do
it 'updates updated_at' do
- expect {described_class.new(parent, user, params).execute(issue)}
- .to change {issue.reload.updated_at}
+ expect { described_class.new(parent, user, params).execute(issue) }
+ .to change { issue.reload.updated_at }
end
end
@@ -140,6 +140,40 @@ RSpec.shared_examples 'issues move service' do |group|
expect(issue2.reload.updated_at.change(usec: 0)).to eq updated_at2.change(usec: 0)
end
+ context 'when moving to a specific list position' do
+ before do
+ [issue1, issue2, issue].each do |issue|
+ issue.move_to_end && issue.save!
+ end
+ end
+
+ it 'moves issue to the top of the list' do
+ described_class.new(parent, user, params.merge({ position_in_list: 0 })).execute(issue)
+
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'moves issue to a position in the middle of the list' do
+ described_class.new(parent, user, params.merge({ position_in_list: 1 })).execute(issue)
+
+ expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ end
+
+ it 'moves issue to the bottom of the list' do
+ described_class.new(parent, user, params.merge({ position_in_list: -1 })).execute(issue1)
+
+ expect(issue1.relative_position).to be > issue.relative_position
+ end
+
+ context 'when given position is greater than number of issues in the list' do
+ it 'moves the issue to the bottom of the list' do
+ described_class.new(parent, user, params.merge({ position_in_list: 5 })).execute(issue1)
+
+ expect(issue1.relative_position).to be > issue.relative_position
+ end
+ end
+ end
+
def reorder_issues(params, issues: [])
issues.each do |issue|
issue.move_to_end && issue.save!
diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
index ce412ef55de..1887b38b50e 100644
--- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
+++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
@@ -42,6 +42,7 @@ RSpec.shared_examples 'a system note' do |params|
it 'has the correct attributes', :aggregate_failures do
exclude_project = !params.nil? && params[:exclude_project]
+ skip_persistence_check = !params.nil? && params[:skip_persistence_check]
expect(subject).to be_valid
expect(subject).to be_system
@@ -50,6 +51,7 @@ RSpec.shared_examples 'a system note' do |params|
expect(subject.project).to eq project unless exclude_project
expect(subject.author).to eq author
+ expect(subject.system_note_metadata).to be_persisted unless skip_persistence_check
expect(subject.system_note_metadata.action).to eq(action)
expect(subject.system_note_metadata.commit_count).to eq(commit_count)
end
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index 3be59af6a37..58659775d8c 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -54,6 +54,12 @@ RSpec.shared_examples 'a valid token' do
end
end
+RSpec.shared_examples 'with auth_type' do
+ let(:current_params) { super().merge(auth_type: :foo) }
+
+ it { expect(payload['auth_type']).to eq('foo') }
+end
+
RSpec.shared_examples 'a browsable' do
let(:access) do
[{ 'type' => 'registry',
@@ -199,8 +205,8 @@ RSpec.shared_examples 'a container registry auth service' do
describe '.import_access_token' do
let(:access) do
[{ 'type' => 'registry',
- 'name' => 'import',
- 'actions' => ['*'] }]
+ 'name' => 'import',
+ 'actions' => ['*'] }]
end
let(:token) { described_class.import_access_token }
@@ -286,6 +292,7 @@ RSpec.shared_examples 'a container registry auth service' do
shared_examples 'private project' do
context 'allow to use scope-less authentication' do
it_behaves_like 'a valid token'
+ it_behaves_like 'with auth_type'
end
context 'allow developer to push images' do
@@ -299,6 +306,7 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pushable'
it_behaves_like 'container repository factory'
+ it_behaves_like 'with auth_type'
end
context 'disallow developer to delete images' do
@@ -341,6 +349,7 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pullable'
it_behaves_like 'not a container repository factory'
+ it_behaves_like 'with auth_type'
end
end
@@ -381,6 +390,7 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pullable'
it_behaves_like 'not a container repository factory'
+ it_behaves_like 'with auth_type'
end
context 'disallow guest to pull or push images' do
@@ -445,6 +455,7 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pullable'
it_behaves_like 'not a container repository factory'
+ it_behaves_like 'with auth_type'
end
context 'disallow anyone to push images' do
@@ -495,6 +506,7 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pullable'
it_behaves_like 'not a container repository factory'
+ it_behaves_like 'with auth_type'
end
context 'disallow anyone to push images' do
@@ -600,6 +612,7 @@ RSpec.shared_examples 'a container registry auth service' do
end
it_behaves_like 'a valid token'
+ it_behaves_like 'with auth_type'
context 'allow to pull and push images' do
let(:current_params) do
@@ -944,10 +957,11 @@ RSpec.shared_examples 'a container registry auth service' do
shared_examples 'able to login' do
context 'registry provides read_container_image authentication_abilities' do
- let(:current_params) { { deploy_token: deploy_token } }
+ let(:current_params) { { deploy_token: deploy_token, auth_type: :deploy_token } }
let(:authentication_abilities) { [:read_container_image] }
it_behaves_like 'an authenticated'
+ it { expect(payload['auth_type']).to eq('deploy_token') }
end
end
diff --git a/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb b/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb
index a62cffc0e1b..73a02905914 100644
--- a/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb
+++ b/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples_for 'update feature flag client' do
+RSpec.shared_examples_for 'update feature flag client' do
let!(:client) { create(:operations_feature_flags_client, project: project) }
it 'updates last feature flag updated at' do
@@ -10,7 +10,7 @@ shared_examples_for 'update feature flag client' do
end
end
-shared_examples_for 'does not update feature flag client' do
+RSpec.shared_examples_for 'does not update feature flag client' do
let!(:client) { create(:operations_feature_flags_client, project: project) }
it 'does not update last feature flag updated at' do
diff --git a/spec/support/shared_examples/services/gitlab_projects_import_service_shared_examples.rb b/spec/support/shared_examples/services/gitlab_projects_import_service_shared_examples.rb
index 2aac7e328f0..366fa4763e1 100644
--- a/spec/support/shared_examples/services/gitlab_projects_import_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/gitlab_projects_import_service_shared_examples.rb
@@ -33,7 +33,7 @@ RSpec.shared_examples 'gitlab projects import validations' do
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
- let(:path) { existing_project.path}
+ let(:path) { existing_project.path }
it 'does not create the project' do
project = subject.execute
diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
index 31571b1ffb9..92681b8ba79 100644
--- a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples_for 'service scheduling async deletes' do
+RSpec.shared_examples_for 'service scheduling async deletes' do
it 'destroys associated todos asynchronously' do
expect(worker_class)
.to receive(:perform_async)
@@ -20,13 +20,13 @@ shared_examples_for 'service scheduling async deletes' do
end
end
-shared_examples_for 'service deleting todos' do
+RSpec.shared_examples_for 'service deleting todos' do
it_behaves_like 'service scheduling async deletes' do
let(:worker_class) { TodosDestroyer::DestroyedIssuableWorker }
end
end
-shared_examples_for 'service deleting label links' do
+RSpec.shared_examples_for 'service deleting label links' do
it_behaves_like 'service scheduling async deletes' do
let(:worker_class) { Issuable::LabelLinksDestroyWorker }
end
diff --git a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
new file mode 100644
index 00000000000..3d90885dd6f
--- /dev/null
+++ b/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'issuable update service updating last_edited_at values' do
+ context 'when updating the title of the issuable' do
+ let(:update_params) { { title: 'updated title' } }
+
+ it 'does not update last_edited values' do
+ expect { update_issuable }.to change(issuable, :title).from(issuable.title).to('updated title').and(
+ not_change(issuable, :last_edited_at)
+ ).and(
+ not_change(issuable, :last_edited_by)
+ )
+ end
+ end
+
+ context 'when updating the description of the issuable' do
+ let(:update_params) { { description: 'updated description' } }
+
+ it 'updates last_edited values' do
+ expect do
+ update_issuable
+ end.to change(issuable, :description).from(issuable.description).to('updated description').and(
+ change(issuable, :last_edited_at)
+ ).and(
+ change(issuable, :last_edited_by)
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
index 9610cdd18a3..65351ac94ab 100644
--- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples 'issuable link creation' do
+RSpec.shared_examples 'issuable link creation' do
describe '#execute' do
subject { described_class.new(issuable, user, params).execute }
diff --git a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb
index 53d637a9094..5e80014da1d 100644
--- a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples 'a destroyable issuable link' do
+RSpec.shared_examples 'a destroyable issuable link' do
context 'when successfully removes an issuable link' do
before do
issuable_link.source.resource_parent.add_reporter(user)
diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb
index d2595b92cbc..b3ba0a1be93 100644
--- a/spec/support/shared_examples/services/merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb
@@ -123,7 +123,7 @@ end
RSpec.shared_examples 'with an existing branch that has a merge request open' do |count|
let(:changes) { existing_branch_changes }
- let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) }
it_behaves_like 'a service that does not create a merge request'
it_behaves_like 'a service that can change assignees of a merge request', count
diff --git a/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb b/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb
index 8c6c2271af3..07025dac689 100644
--- a/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb
+++ b/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples 'records an onboarding progress action' do |action|
include AfterNextHelpers
it do
- expect_next(OnboardingProgressService, namespace)
+ expect_next(Onboarding::ProgressService, namespace)
.to receive(:execute).with(action: action).and_call_original
subject
@@ -13,7 +13,7 @@ end
RSpec.shared_examples 'does not record an onboarding progress action' do
it do
- expect(OnboardingProgressService).not_to receive(:new)
+ expect(Onboarding::ProgressService).not_to receive(:new)
subject
end
diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
index 7fd20fc3909..ea79dc674a1 100644
--- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
@@ -190,6 +190,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Codename: unstable
Date: Sat, 25 Jan 2020 15:17:18 +0000
Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Acquire-By-Hash: yes
Architectures: all amd64 arm64
Components: contrib main
MD5Sum:
@@ -249,6 +250,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Codename: unstable
Date: Sat, 25 Jan 2020 15:17:18 +0000
Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Acquire-By-Hash: yes
MD5Sum:
SHA256:
EOF
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index 704a4bbe0b8..ca4dea90c55 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -227,6 +227,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package10) { create(:rubygems_package, project: project) }
let_it_be(:package11) { create(:helm_package, project: project) }
let_it_be(:package12) { create(:terraform_module_package, project: project) }
+ let_it_be(:package13) { create(:rpm_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
index 716bee39fca..a7e51408032 100644
--- a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'filters by paginated notes' do |event_type|
- let(:event) { create(event_type) } # rubocop:disable Rails/SaveBang
+ let(:event) { create(event_type, issue: create(:issue)) }
before do
create(event_type, issue: event.issue)
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
index 5a44f739b27..65893d84798 100644
--- a/spec/support/shared_examples/services/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -14,7 +14,8 @@ RSpec.shared_examples 'checking spam' do
spammable: kind_of(Snippet),
spam_params: spam_params,
user: an_instance_of(User),
- action: action
+ action: action,
+ extra_features: { files: an_instance_of(Array) }
}
) do |instance|
expect(instance).to receive(:execute)
@@ -24,7 +25,7 @@ RSpec.shared_examples 'checking spam' do
end
end
-shared_examples 'invalid params error response' do
+RSpec.shared_examples 'invalid params error response' do
before do
allow_next_instance_of(described_class) do |service|
allow(service).to receive(:valid_params?).and_return false
diff --git a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
index 0687be6f429..31919a4263d 100644
--- a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-shared_examples 'issue_edit snowplow tracking' do
+RSpec.shared_examples 'issue_edit snowplow tracking' do
let(:category) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CATEGORY }
let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION }
let(:label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL }
diff --git a/spec/support/shared_examples/tasks/gitlab/uploads/migration_shared_examples.rb b/spec/support/shared_examples/tasks/gitlab/uploads/migration_shared_examples.rb
deleted file mode 100644
index b37a8059574..00000000000
--- a/spec/support/shared_examples/tasks/gitlab/uploads/migration_shared_examples.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-# Expects the calling spec to define:
-# - uploader_class
-# - model_class
-# - mounted_as
-RSpec.shared_examples 'enqueue upload migration jobs in batch' do |batch:|
- def run(task)
- args = [uploader_class.to_s, model_class.to_s, mounted_as].compact
- run_rake_task(task, *args)
- end
-
- it 'migrates local storage to remote object storage' do
- expect(ObjectStorage::MigrateUploadsWorker)
- .to receive(:perform_async).exactly(batch).times
- .and_return("A fake job.")
-
- run('gitlab:uploads:migrate')
- end
-
- it 'migrates remote object storage to local storage' do
- expect(Upload).to receive(:where).exactly(batch + 1).times { Upload.all }
- expect(ObjectStorage::MigrateUploadsWorker)
- .to receive(:perform_async)
- .with(anything, model_class.name, mounted_as, ObjectStorage::Store::LOCAL)
- .exactly(batch).times
- .and_return("A fake job.")
-
- run('gitlab:uploads:migrate_to_local')
- end
-end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index f8b00d1e4c0..3c977e62a10 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -56,8 +56,8 @@ RSpec.shared_examples "migrates" do |to_store:, from_store: nil|
it 'can access to the original file during migration' do
file = subject.file
- allow(subject).to receive(:delete_migrated_file) { } # Remove as a callback of :migrate
- allow(subject).to receive(:record_upload) { } # Remove as a callback of :store (:record_upload)
+ allow(subject).to receive(:delete_migrated_file) {} # Remove as a callback of :migrate
+ allow(subject).to receive(:record_upload) {} # Remove as a callback of :store (:record_upload)
expect(file.exists?).to be_truthy
expect { migrate(to) }.not_to change { file.exists? }
diff --git a/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb
new file mode 100644
index 00000000000..eb03f0888b9
--- /dev/null
+++ b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'migrating records to the ghost user' do |record_class, fields|
+ record_class_name = record_class.to_s.titleize.downcase
+
+ let(:project) do
+ case record_class
+ when MergeRequest
+ create(:project, :repository)
+ else
+ create(:project)
+ end
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ context "for a #{record_class_name} the user has created" do
+ let!(:record) { created_record }
+ let(:migrated_fields) { fields || [:author] }
+
+ it "does not delete the #{record_class_name}" do
+ service.execute
+
+ expect(record_class.find_by_id(record.id)).to be_present
+ end
+
+ it 'migrates all associated fields to the "Ghost user"' do
+ service.execute
+
+ migrated_record = record_class.find_by_id(record.id)
+
+ migrated_fields.each do |field|
+ expect(migrated_record.public_send(field)).to eq(User.ghost)
+ end
+ end
+ end
+end
diff --git a/spec/support_specs/database/without_check_constraint_spec.rb b/spec/support_specs/database/without_check_constraint_spec.rb
new file mode 100644
index 00000000000..d78eafd4a32
--- /dev/null
+++ b/spec/support_specs/database/without_check_constraint_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Database::WithoutCheckConstraint' do
+ include MigrationsHelpers
+
+ describe '.without_check_constraint' do
+ let(:connection) { ApplicationRecord.connection }
+ let(:table_name) { '_test_table' }
+ let(:constraint_name) { 'check_1234' }
+ let(:model) { table(table_name) }
+
+ before do
+ # Drop test table in case it's left from a previous execution.
+ connection.exec_query("DROP TABLE IF EXISTS #{table_name}")
+ # Model has an attribute called 'name' that can't be NULL.
+ connection.exec_query(<<-SQL)
+ CREATE TABLE #{table_name} (
+ name text
+ CONSTRAINT #{constraint_name} CHECK (name IS NOT NULL)
+ );
+ SQL
+ end
+
+ context 'with invalid table' do
+ subject do
+ without_check_constraint('no_such_table', constraint_name, connection: connection) {}
+ end
+
+ it 'raises exception' do
+ msg = "'no_such_table' does not exist"
+ expect { subject }.to raise_error(msg)
+ end
+ end
+
+ context 'with invalid constraint name' do
+ subject do
+ without_check_constraint(table_name, 'no_such_constraint', connection: connection) {}
+ end
+
+ it 'raises exception' do
+ msg = "'#{table_name}' table does not contain constraint called 'no_such_constraint'"
+ expect { subject }.to raise_error(msg)
+ end
+ end
+
+ context 'with constraint' do
+ subject { connection.check_constraints(table_name) }
+
+ it 'removes inside block' do
+ without_check_constraint(table_name, constraint_name, connection: connection) do
+ expect(subject).to be_empty
+ end
+ end
+
+ it 'restores outside block' do
+ saved_constraints = subject
+
+ without_check_constraint(table_name, constraint_name, connection: connection) do
+ end
+
+ expect(subject).to eq(saved_constraints)
+ end
+ end
+
+ context 'when creating an invalid record' do
+ subject(:invalid_record) { model.create!(name: nil) }
+
+ it 'enables invalid record creation inside block' do
+ without_check_constraint(table_name, constraint_name, connection: connection) do
+ expect(invalid_record).to be_persisted
+ expect(invalid_record.name).to be_nil
+ end
+ end
+
+ it 'rolls back changes made within the block' do
+ without_check_constraint(table_name, constraint_name, connection: connection) do
+ invalid_record
+ end
+ expect(model.all).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/support_specs/helpers/html_escaped_helpers_spec.rb b/spec/support_specs/helpers/html_escaped_helpers_spec.rb
new file mode 100644
index 00000000000..337f7ecc659
--- /dev/null
+++ b/spec/support_specs/helpers/html_escaped_helpers_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../support/helpers/html_escaped_helpers'
+
+RSpec.describe HtmlEscapedHelpers do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#match_html_escaped_tags' do
+ let(:actual_match) { actual_match_data && actual_match_data[0] }
+
+ subject(:actual_match_data) { described_class.match_html_escaped_tags(content) }
+
+ where(:content, :expected_match) do
+ nil | nil
+ '' | nil
+ '<a href' | nil
+ '<span href' | nil
+ '</a>' | nil
+ '&lt;a href' | '&lt;a'
+ '&lt;span href' | '&lt;span'
+ '&lt; span' | '&lt; span'
+ 'some text &lt;a href' | '&lt;a'
+ 'some text "&lt;a href' | '&lt;a'
+ '&lt;/a&glt;' | '&lt;/a'
+ '&lt;/span&gt;' | '&lt;/span'
+ '&lt; / span&gt;' | '&lt; / span'
+ 'title="&lt;a href' | nil
+ 'title= "&lt;a href' | nil
+ "title= '&lt;a href" | nil
+ "title= '&lt;/a" | nil
+ "title= '&lt;/span" | nil
+ 'title="foo">&lt;a' | '&lt;a'
+ "title='foo'>\n&lt;a" | '&lt;a'
+ end
+
+ with_them do
+ specify { expect(actual_match).to eq(expected_match) }
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb b/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb
new file mode 100644
index 00000000000..f9ebb985255
--- /dev/null
+++ b/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:db:truncate_legacy_tables', :silence_stdout, :reestablished_active_record_base,
+ :suppress_gitlab_schemas_validate_connection do
+ let(:main_connection) { ApplicationRecord.connection }
+ let(:ci_connection) { Ci::ApplicationRecord.connection }
+ let(:test_gitlab_main_table) { '_test_gitlab_main_table' }
+ let(:test_gitlab_ci_table) { '_test_gitlab_ci_table' }
+
+ before :all do
+ Rake.application.rake_require 'active_record/railties/databases'
+ Rake.application.rake_require 'tasks/seed_fu'
+ Rake.application.rake_require 'tasks/gitlab/db/validate_config'
+ Rake.application.rake_require 'tasks/gitlab/db/truncate_legacy_tables'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ before do
+ skip_if_multiple_databases_not_setup
+
+ # Filling the table on both databases main and ci
+ Gitlab::Database.database_base_models.each_value do |base_model|
+ base_model.connection.execute(<<~SQL)
+ CREATE TABLE #{test_gitlab_main_table} (id integer NOT NULL);
+ INSERT INTO #{test_gitlab_main_table} VALUES(generate_series(1, 50));
+ SQL
+ base_model.connection.execute(<<~SQL)
+ CREATE TABLE #{test_gitlab_ci_table} (id integer NOT NULL);
+ INSERT INTO #{test_gitlab_ci_table} VALUES(generate_series(1, 50));
+ SQL
+ end
+
+ allow(Gitlab::Database::GitlabSchema).to receive(:tables_to_schema).and_return(
+ {
+ test_gitlab_main_table => :gitlab_main,
+ test_gitlab_ci_table => :gitlab_ci
+ }
+ )
+ end
+
+ shared_examples 'truncating legacy tables' do
+ before do
+ allow(ENV).to receive(:[]).and_return(nil)
+ end
+
+ context 'when tables are not locked for writes' do
+ it 'raises an error when trying to truncate the tables' do
+ error_message = /is not locked for writes. Run the rake task gitlab:db:lock_writes first/
+ expect { truncate_legacy_tables }.to raise_error(error_message)
+ end
+ end
+
+ context 'when tables are locked for writes' do
+ before do
+ # Locking ci table on the main database
+ Gitlab::Database::LockWritesManager.new(
+ table_name: test_gitlab_ci_table,
+ connection: main_connection,
+ database_name: "main"
+ ).lock_writes
+
+ # Locking main table on the ci database
+ Gitlab::Database::LockWritesManager.new(
+ table_name: test_gitlab_main_table,
+ connection: ci_connection,
+ database_name: "ci"
+ ).lock_writes
+ end
+
+ it 'calls TablesTruncate with the correct parameters and default minimum batch size' do
+ expect(Gitlab::Database::TablesTruncate).to receive(:new).with(
+ database_name: database_name,
+ min_batch_size: 5,
+ logger: anything,
+ dry_run: false,
+ until_table: nil
+ ).and_call_original
+
+ truncate_legacy_tables
+ end
+
+ it 'truncates the legacy table' do
+ expect do
+ truncate_legacy_tables
+ end.to change { connection.select_value("SELECT count(*) from #{legacy_table}") }.from(50).to(0)
+ end
+
+ it 'does not truncate the table that belongs to the connection schema' do
+ expect do
+ truncate_legacy_tables
+ end.not_to change { connection.select_value("SELECT count(*) from #{active_table}") }
+ end
+
+ context 'when running in dry_run mode' do
+ before do
+ allow(ENV).to receive(:[]).with("DRY_RUN").and_return("true")
+ end
+
+ it 'does not truncate any tables' do
+ expect do
+ truncate_legacy_tables
+ end.not_to change { connection.select_value("SELECT count(*) from #{legacy_table}") }
+ end
+
+ it 'prints the truncation sql statement to the output' do
+ expect do
+ truncate_legacy_tables
+ end.to output(/TRUNCATE TABLE #{legacy_table} RESTRICT/).to_stdout
+ end
+ end
+
+ context 'when passing until_table parameter via environment variable' do
+ before do
+ allow(ENV).to receive(:[]).with("UNTIL_TABLE").and_return(legacy_table)
+ end
+
+ it 'sends the table name to TablesTruncate' do
+ expect(Gitlab::Database::TablesTruncate).to receive(:new).with(
+ database_name: database_name,
+ min_batch_size: 5,
+ logger: anything,
+ dry_run: false,
+ until_table: legacy_table
+ ).and_call_original
+
+ truncate_legacy_tables
+ end
+ end
+ end
+ end
+
+ context 'when truncating ci tables on the main database' do
+ subject(:truncate_legacy_tables) { run_rake_task('gitlab:db:truncate_legacy_tables:main') }
+
+ let(:connection) { ApplicationRecord.connection }
+ let(:database_name) { 'main' }
+ let(:active_table) { test_gitlab_main_table }
+ let(:legacy_table) { test_gitlab_ci_table }
+
+ it_behaves_like 'truncating legacy tables'
+ end
+
+ context 'when truncating main tables on the ci database' do
+ subject(:truncate_legacy_tables) { run_rake_task('gitlab:db:truncate_legacy_tables:ci') }
+
+ let(:connection) { Ci::ApplicationRecord.connection }
+ let(:database_name) { 'ci' }
+ let(:active_table) { test_gitlab_ci_table }
+ let(:legacy_table) { test_gitlab_main_table }
+
+ it_behaves_like 'truncating legacy tables'
+ end
+end
diff --git a/spec/tasks/gitlab/db/validate_config_rake_spec.rb b/spec/tasks/gitlab/db/validate_config_rake_spec.rb
index ad15c7f0d1c..1d47c94aa77 100644
--- a/spec/tasks/gitlab/db/validate_config_rake_spec.rb
+++ b/spec/tasks/gitlab/db/validate_config_rake_spec.rb
@@ -216,7 +216,7 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_sc
let(:exception) { ActiveRecord::StatementInvalid.new("READONLY") }
before do
- allow(exception).to receive(:cause).and_return(PG::ReadOnlySqlTransaction.new("cannot execute INSERT in a read-only transaction"))
+ allow(exception).to receive(:cause).and_return(PG::ReadOnlySqlTransaction.new("cannot execute UPSERT in a read-only transaction"))
allow(ActiveRecord::InternalMetadata).to receive(:upsert).at_least(:once).and_raise(exception)
end
diff --git a/spec/tasks/gitlab/snippets_rake_spec.rb b/spec/tasks/gitlab/snippets_rake_spec.rb
index c55bded1d5a..c50b04b4600 100644
--- a/spec/tasks/gitlab/snippets_rake_spec.rb
+++ b/spec/tasks/gitlab/snippets_rake_spec.rb
@@ -3,7 +3,7 @@
require 'rake_helper'
RSpec.describe 'gitlab:snippets namespace rake task', :silence_stdout do
- let_it_be(:user) { create(:user)}
+ let_it_be(:user) { create(:user) }
let_it_be(:migrated) { create(:personal_snippet, :repository, author: user) }
let(:non_migrated) { create_list(:personal_snippet, 3, author: user) }
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index e293271ca67..3a368a5011b 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -2,133 +2,93 @@
require 'rake_helper'
-RSpec.describe 'gitlab:uploads:migrate and migrate_to_local rake tasks', :silence_stdout do
- let(:model_class) { nil }
- let(:uploader_class) { nil }
- let(:mounted_as) { nil }
- let(:batch_size) { 3 }
-
+RSpec.describe 'gitlab:uploads:migrate and migrate_to_local rake tasks', :sidekiq_inline, :silence_stdout do
before do
- stub_env('MIGRATION_BATCH_SIZE', batch_size.to_s)
- stub_uploads_object_storage(uploader_class)
+ stub_env('MIGRATION_BATCH_SIZE', 3.to_s)
+ stub_uploads_object_storage(AvatarUploader)
+ stub_uploads_object_storage(FileUploader)
Rake.application.rake_require 'tasks/gitlab/uploads/migrate'
- allow(ObjectStorage::MigrateUploadsWorker).to receive(:perform_async)
+ create_list(:project, 2, :with_avatar)
+ create_list(:group, 2, :with_avatar)
+ create_list(:project, 2) do |model|
+ FileUploader.new(model).store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
end
- context "for AvatarUploader" do
- let(:uploader_class) { AvatarUploader }
- let(:mounted_as) { :avatar }
+ let(:total_uploads_count) { 6 }
- context "for Project" do
- let(:model_class) { Project }
- let!(:projects) { create_list(:project, 10, :with_avatar) }
+ it 'migrates all uploads to object storage in batches' do
+ expect(ObjectStorage::MigrateUploadsWorker)
+ .to receive(:perform_async).twice.and_call_original
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
- end
+ run_rake_task('gitlab:uploads:migrate:all')
- context "for Group" do
- let(:model_class) { Group }
+ expect(Upload.with_files_stored_locally.count).to eq(0)
+ expect(Upload.with_files_stored_remotely.count).to eq(total_uploads_count)
+ end
- before do
- create_list(:group, 10, :with_avatar)
- end
+ it 'migrates all uploads to local storage in batches' do
+ run_rake_task('gitlab:uploads:migrate')
+ expect(Upload.with_files_stored_remotely.count).to eq(total_uploads_count)
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
- end
+ expect(ObjectStorage::MigrateUploadsWorker)
+ .to receive(:perform_async).twice.and_call_original
- context "for User" do
- let(:model_class) { User }
+ run_rake_task('gitlab:uploads:migrate_to_local:all')
- before do
- create_list(:user, 10, :with_avatar)
- end
-
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
- end
+ expect(Upload.with_files_stored_remotely.count).to eq(0)
+ expect(Upload.with_files_stored_locally.count).to eq(total_uploads_count)
end
- context "for AttachmentUploader" do
- let(:uploader_class) { AttachmentUploader }
+ shared_examples 'migrate task with filters' do
+ it 'migrates matching uploads to object storage' do
+ run_rake_task('gitlab:uploads:migrate', task_arguments)
- context "for Note" do
- let(:model_class) { Note }
- let(:mounted_as) { :attachment }
+ migrated_count = matching_uploads.with_files_stored_remotely.count
- before do
- create_list(:note, 10, :with_attachment)
- end
-
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
+ expect(migrated_count).to eq(matching_uploads.count)
+ expect(Upload.with_files_stored_locally.count).to eq(total_uploads_count - migrated_count)
end
- context "for Appearance" do
- let(:model_class) { Appearance }
- let(:mounted_as) { :logo }
+ it 'migrates matching uploads to local storage' do
+ run_rake_task('gitlab:uploads:migrate')
+ expect(Upload.with_files_stored_remotely.count).to eq(total_uploads_count)
+
+ run_rake_task('gitlab:uploads:migrate_to_local', task_arguments)
- before do
- create(:appearance, :with_logos)
- end
+ migrated_count = matching_uploads.with_files_stored_locally.count
- %i(logo header_logo).each do |mount|
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 1 do
- let(:mounted_as) { mount }
- end
- end
+ expect(migrated_count).to eq(matching_uploads.count)
+ expect(Upload.with_files_stored_remotely.count).to eq(total_uploads_count - migrated_count)
end
end
- context "for FileUploader" do
- let(:uploader_class) { FileUploader }
- let(:model_class) { Project }
+ context 'when uploader_class is given' do
+ let(:task_arguments) { ['FileUploader'] }
+ let(:matching_uploads) { Upload.where(uploader: 'FileUploader') }
- before do
- create_list(:project, 10) do |model|
- uploader_class.new(model)
- .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
- end
- end
-
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
+ it_behaves_like 'migrate task with filters'
end
- context "for PersonalFileUploader" do
- let(:uploader_class) { PersonalFileUploader }
- let(:model_class) { PersonalSnippet }
-
- before do
- create_list(:personal_snippet, 10) do |model|
- uploader_class.new(model)
- .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
- end
- end
+ context 'when model_class is given' do
+ let(:task_arguments) { [nil, 'Project'] }
+ let(:matching_uploads) { Upload.where(model_type: 'Project') }
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
+ it_behaves_like 'migrate task with filters'
end
- context "for NamespaceFileUploader" do
- let(:uploader_class) { NamespaceFileUploader }
- let(:model_class) { Snippet }
+ context 'when mounted_as is given' do
+ let(:task_arguments) { [nil, nil, :avatar] }
+ let(:matching_uploads) { Upload.where(mount_point: :avatar) }
- before do
- create_list(:snippet, 10) do |model|
- uploader_class.new(model)
- .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
- end
- end
-
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
+ it_behaves_like 'migrate task with filters'
end
- context 'for DesignManagement::DesignV432x230Uploader' do
- let(:uploader_class) { DesignManagement::DesignV432x230Uploader }
- let(:model_class) { DesignManagement::Action }
- let(:mounted_as) { :image_v432x230 }
-
- before do
- create_list(:design_action, 10, :with_image_v432x230)
- end
+ context 'when multiple filters are given' do
+ let(:task_arguments) { %w[AvatarUploader Project] }
+ let(:matching_uploads) { Upload.where(uploader: 'AvatarUploader', model_type: 'Project') }
- it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
+ it_behaves_like 'migrate task with filters'
end
end
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
index 442b884b313..f54d06f406f 100644
--- a/spec/tasks/gitlab/usage_data_rake_spec.rb
+++ b/spec/tasks/gitlab/usage_data_rake_spec.rb
@@ -3,15 +3,23 @@
require 'rake_helper'
RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
+ include StubRequests
include UsageDataHelpers
+ let(:metrics_file) { Rails.root.join('tmp', 'test', 'sql_metrics_queries.json') }
+
before do
Rake.application.rake_require 'tasks/gitlab/usage_data'
+
# stub prometheus external http calls https://gitlab.com/gitlab-org/gitlab/-/issues/245277
stub_prometheus_queries
stub_database_flavor_check
end
+ after do
+ FileUtils.rm_rf(metrics_file)
+ end
+
describe 'dump_sql_in_yaml' do
it 'dumps SQL queries in yaml format' do
expect { run_rake_task('gitlab:usage_data:dump_sql_in_yaml') }.to output(/.*recorded_at:.*/).to_stdout
@@ -23,4 +31,53 @@ RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
expect { run_rake_task('gitlab:usage_data:dump_sql_in_json') }.to output(/.*"recorded_at":.*/).to_stdout
end
end
+
+ describe 'dump_non_sql_in_json' do
+ it 'dumps non SQL data in json format' do
+ expect { run_rake_task('gitlab:usage_data:dump_non_sql_in_json') }.to output(/.*"recorded_at":.*/).to_stdout
+ end
+ end
+
+ describe 'generate_sql_metrics_fixture' do
+ it 'generates fixture file correctly' do
+ run_rake_task('gitlab:usage_data:generate_sql_metrics_queries')
+
+ expect(Pathname.new(metrics_file)).to exist
+ end
+ end
+
+ describe 'generate_and_send' do
+ let(:service_ping_payload_url) do
+ File.join(ServicePing::SubmitService::STAGING_BASE_URL, ServicePing::SubmitService::USAGE_DATA_PATH)
+ end
+
+ let(:service_ping_metadata_url) do
+ File.join(ServicePing::SubmitService::STAGING_BASE_URL, ServicePing::SubmitService::METADATA_PATH)
+ end
+
+ let(:payload) { { recorded_at: Time.current } }
+
+ before do
+ allow_next_instance_of(ServicePing::BuildPayload) do |service|
+ allow(service).to receive(:execute).and_return(payload)
+ end
+ stub_response(body: payload.merge(conv_index: { usage_data_id: 123 }))
+ stub_response(body: nil, url: service_ping_metadata_url, status: 201)
+ end
+
+ it 'generates and sends Service Ping payload' do
+ expect { run_rake_task('gitlab:usage_data:generate_and_send') }.to output(/.*201.*/).to_stdout
+ end
+
+ private
+
+ def stub_response(url: service_ping_payload_url, body:, status: 201)
+ stub_full_request(url, method: :post)
+ .to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: body.to_json,
+ status: status
+ )
+ end
+ end
end
diff --git a/spec/tasks/rubocop_rake_spec.rb b/spec/tasks/rubocop_rake_spec.rb
index a92d7dc2e52..eb360cdff93 100644
--- a/spec/tasks/rubocop_rake_spec.rb
+++ b/spec/tasks/rubocop_rake_spec.rb
@@ -3,16 +3,20 @@
require 'fast_spec_helper'
require 'rake'
+require 'tmpdir'
require 'fileutils'
require_relative '../support/silence_stdout'
require_relative '../support/helpers/next_instance_of'
require_relative '../support/helpers/rake_helpers'
+require_relative '../support/matchers/abort_matcher'
require_relative '../../rubocop/formatter/todo_formatter'
require_relative '../../rubocop/todo_dir'
+require_relative '../../rubocop/check_graceful_task'
RSpec.describe 'rubocop rake tasks', :silence_stdout do
include RakeHelpers
+ include NextInstanceOf
before do
stub_const('Rails', double(:rails_env))
@@ -23,6 +27,41 @@ RSpec.describe 'rubocop rake tasks', :silence_stdout do
Rake.application.rake_require 'tasks/rubocop'
end
+ describe 'check:graceful' do
+ let(:options) { %w[file.rb Cop/Name] }
+
+ subject(:run_task) { run_rake_task('rubocop:check:graceful', *options) }
+
+ before do
+ allow_next_instance_of(RuboCop::CheckGracefulTask, $stdout) do |task|
+ allow(task).to receive(:run).with(options).and_return(task_result)
+ end
+ end
+
+ context 'with successful task result' do
+ let(:task_result) { 0 }
+
+ # We cannot use `abort_execution` because it's ignoring exit status `0`.
+ # Rely on SystemExitDetected here.
+ specify { run_task }
+
+ it 'modifies ENV and deletes REVEAL_RUBOCOP_TODO key' do
+ # There's ENV backup in before block.
+ ENV['REVEAL_RUBOCOP_TODO'] = '0' # rubocop:disable RSpec/EnvAssignment
+
+ run_task
+
+ expect(ENV.key?('REVEAL_RUBOCOP_TODO')).to eq(false)
+ end
+ end
+
+ context 'with non-successful task result' do
+ let(:task_result) { 1 }
+
+ specify { expect { run_task }.to abort_execution }
+ end
+ end
+
describe 'todo:generate', :aggregate_failures do
let(:tmp_dir) { Dir.mktmpdir }
let(:rubocop_todo_dir) { File.join(tmp_dir, '.rubocop_todo') }
diff --git a/spec/tooling/danger/config_files_spec.rb b/spec/tooling/danger/config_files_spec.rb
new file mode 100644
index 00000000000..0e01908a1dd
--- /dev/null
+++ b/spec/tooling/danger/config_files_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'gitlab-dangerfiles'
+require 'danger'
+require 'danger/plugins/internal/helper'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/config_files'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::ConfigFiles do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+ let(:matching_line) { "+ introduced_by_url:" }
+
+ subject(:config_file) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(config_file).to receive(:project_helper).and_return(fake_project_helper)
+ end
+
+ describe '#add_suggestion_for_missing_introduced_by_url' do
+ let(:file_lines) do
+ [
+ "---",
+ "name: about_your_company_registration_flow",
+ "introduced_by_url: #{url}",
+ "rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355909",
+ "milestone: '14.10'"
+ ]
+ end
+
+ let(:filename) { 'config/feature_flags/new_ff.yml' }
+
+ before do
+ allow(config_file.project_helper).to receive(:file_lines).and_return(file_lines)
+ allow(config_file.helper).to receive(:added_files).and_return([filename])
+ allow(config_file.helper).to receive(:mr_web_url).and_return(url)
+ end
+
+ context 'when config file has an empty introduced_by_url line' do
+ let(:url) { '' }
+
+ it 'adds suggestions at the correct line' do
+ expected_format = format(described_class::SUGGEST_INTRODUCED_BY_COMMENT, url: url)
+ expect(config_file).to receive(:markdown).with(expected_format, file: filename, line: 3)
+
+ config_file.add_suggestion_for_missing_introduced_by_url
+ end
+ end
+
+ context 'when config file has an introduced_by_url line with value' do
+ let(:url) { 'https://gitlab.com/gitlab-org/gitlab/-/issues/1' }
+
+ it 'does not add suggestion' do
+ expect(config_file).not_to receive(:markdown)
+
+ config_file.add_suggestion_for_missing_introduced_by_url
+ end
+ end
+ end
+
+ describe '#new_config_files' do
+ let(:expected_files) do
+ %w[
+ config/feature_flags/first.yml
+ config/events/1234_new_event.yml
+ config/metrics/count_7d/new_metric.yml
+ ]
+ end
+
+ before do
+ all_new_files = %w[
+ app/workers/a.rb
+ doc/events/new_event.md
+ config/feature_flags/first.yml
+ config/events/1234_new_event.yml
+ config/metrics/count_7d/new_metric.yml
+ app/assets/index.js
+ ]
+
+ allow(config_file.helper).to receive(:added_files).and_return(all_new_files)
+ end
+
+ it 'returns added, modified, and renamed_after files by default' do
+ expect(config_file.new_config_files).to match_array(expected_files)
+ end
+ end
+end
diff --git a/spec/tooling/danger/datateam_spec.rb b/spec/tooling/danger/datateam_spec.rb
index e4ab3a6f4b1..de8a93baa27 100644
--- a/spec/tooling/danger/datateam_spec.rb
+++ b/spec/tooling/danger/datateam_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Tooling::Danger::Datateam do
impacted: true,
impacted_files: %w(db/structure.sql)
},
- 'with structure.sql changes and Data Warehouse::Impact Check label' => {
+ 'with structure.sql changes and Data Warehouse::Impact Check label' => {
modified_files: %w(db/structure.sql),
changed_lines: ['+group_id bigint NOT NULL)'],
mr_labels: ['Data Warehouse::Impact Check'],
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 2f52c0fd36c..4cc5df385a5 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -31,6 +31,8 @@ RSpec.describe Tooling::Danger::ProjectHelper do
end
where(:path, :expected_categories) do
+ 'glfm_specification/example_snapshots/prosemirror_json.yml' | [:frontend]
+ 'glfm_specification/input/glfm_anything.yml' | [:frontend, :backend]
'usage_data.rb' | [:database, :backend, :product_intelligence]
'doc/foo.md' | [:docs]
'CONTRIBUTING.md' | [:docs]
diff --git a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
new file mode 100644
index 00000000000..b72f6d66d69
--- /dev/null
+++ b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ObjectStorage::CDN::GoogleCDN,
+ :use_clean_rails_memory_store_caching, :use_clean_rails_redis_caching, :sidekiq_inline do
+ include StubRequests
+
+ let(:key) { SecureRandom.hex }
+ let(:key_name) { 'test-key' }
+ let(:options) { { url: 'https://cdn.gitlab.example.com', key_name: key_name, key: Base64.urlsafe_encode64(key) } }
+ let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
+ let(:headers) { { 'Content-Type' => 'application/json' } }
+ let(:public_ip) { '18.245.0.42' }
+
+ subject { described_class.new(options) }
+
+ before do
+ WebMock.stub_request(:get, GoogleCloud::FetchGoogleIpListService::GOOGLE_IP_RANGES_URL)
+ .to_return(status: 200, body: google_cloud_ips, headers: headers)
+ end
+
+ describe '#use_cdn?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ip_address, :expected) do
+ '34.80.0.1' | false
+ '18.245.0.42' | true
+ '2500:1900:4180:0000:0000:0000:0000:0000' | true
+ '2600:1900:4180:0000:0000:0000:0000:0000' | false
+ '10.10.1.5' | false
+ 'fc00:0000:0000:0000:0000:0000:0000:0000' | false
+ end
+
+ with_them do
+ it { expect(subject.use_cdn?(ip_address)).to eq(expected) }
+ end
+
+ context 'when the key name is missing' do
+ let(:options) { { url: 'https://cdn.gitlab.example.com', key: Base64.urlsafe_encode64(SecureRandom.hex) } }
+
+ it 'returns false' do
+ expect(subject.use_cdn?(public_ip)).to be false
+ end
+ end
+
+ context 'when the key is missing' do
+ let(:options) { { url: 'https://invalid.example.com' } }
+
+ it 'returns false' do
+ expect(subject.use_cdn?(public_ip)).to be false
+ end
+ end
+
+ context 'when the key is invalid' do
+ let(:options) { { key_name: key_name, key: '\0x1' } }
+
+ it 'returns false' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
+ expect(subject.use_cdn?(public_ip)).to be false
+ end
+ end
+
+ context 'when the URL is missing' do
+ let(:options) { { key: Base64.urlsafe_encode64(SecureRandom.hex) } }
+
+ it 'returns false' do
+ expect(subject.use_cdn?(public_ip)).to be false
+ end
+ end
+ end
+
+ describe '#signed_url' do
+ let(:path) { '/path/to/file.txt' }
+
+ it 'returns a valid signed URL' do
+ url = subject.signed_url(path)
+
+ expect(url).to start_with("#{options[:url]}#{path}")
+
+ uri = Addressable::URI.parse(url)
+ parsed_query = Rack::Utils.parse_nested_query(uri.query)
+ signature = parsed_query.delete('Signature')
+
+ signed_url = "#{options[:url]}#{path}?Expires=#{parsed_query['Expires']}&KeyName=#{key_name}"
+ computed_signature = OpenSSL::HMAC.digest('SHA1', key, signed_url)
+
+ aggregate_failures do
+ expect(parsed_query['Expires'].to_i).to be > 0
+ expect(parsed_query['KeyName']).to eq(key_name)
+ expect(signature).to eq(Base64.urlsafe_encode64(computed_signature))
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/object_storage/cdn/google_ip_cache_spec.rb b/spec/uploaders/object_storage/cdn/google_ip_cache_spec.rb
new file mode 100644
index 00000000000..d6568636bc0
--- /dev/null
+++ b/spec/uploaders/object_storage/cdn/google_ip_cache_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ObjectStorage::CDN::GoogleIpCache,
+ :use_clean_rails_memory_store_caching, :use_clean_rails_redis_caching do
+ include StubRequests
+
+ let(:subnets) { [IPAddr.new("34.80.0.0/15"), IPAddr.new("2600:1900:4180::/44")] }
+ let(:public_ip) { '18.245.0.42' }
+
+ describe '.update!' do
+ it 'caches to both L1 and L2 caches' do
+ expect(Gitlab::ProcessMemoryCache.cache_backend.exist?(described_class::GOOGLE_CDN_LIST_KEY)).to be false
+ expect(Rails.cache.exist?(described_class::GOOGLE_CDN_LIST_KEY)).to be false
+
+ described_class.update!(subnets)
+
+ expect(Gitlab::ProcessMemoryCache.cache_backend.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to eq(subnets)
+ expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to eq(subnets)
+ end
+ end
+
+ describe '.ready?' do
+ it 'returns false' do
+ expect(described_class.ready?).to be false
+ end
+
+ it 'returns true' do
+ described_class.update!(subnets)
+
+ expect(described_class.ready?).to be true
+ end
+ end
+
+ describe '.google_ip?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ip_address, :expected) do
+ '34.80.0.1' | true
+ '18.245.0.42' | false
+ '2500:1900:4180:0000:0000:0000:0000:0000' | false
+ '2600:1900:4180:0000:0000:0000:0000:0000' | true
+ '10.10.1.5' | false
+ 'fc00:0000:0000:0000:0000:0000:0000:0000' | false
+ end
+
+ before do
+ described_class.update!(subnets)
+ end
+
+ with_them do
+ it { expect(described_class.google_ip?(ip_address)).to eq(expected) }
+ end
+
+ it 'uses the L2 cache and updates the L1 cache when L1 is missing' do
+ Gitlab::ProcessMemoryCache.cache_backend.delete(described_class::GOOGLE_CDN_LIST_KEY)
+ expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to eq(subnets)
+
+ expect(described_class.google_ip?(public_ip)).to be false
+
+ expect(Gitlab::ProcessMemoryCache.cache_backend.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to eq(subnets)
+ expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to eq(subnets)
+ end
+
+ it 'avoids populating L1 cache if L2 is missing' do
+ Gitlab::ProcessMemoryCache.cache_backend.delete(described_class::GOOGLE_CDN_LIST_KEY)
+ Rails.cache.delete(described_class::GOOGLE_CDN_LIST_KEY)
+
+ expect(described_class.google_ip?(public_ip)).to be false
+
+ expect(Gitlab::ProcessMemoryCache.cache_backend.exist?(described_class::GOOGLE_CDN_LIST_KEY)).to be false
+ expect(Rails.cache.exist?(described_class::GOOGLE_CDN_LIST_KEY)).to be false
+ end
+ end
+
+ describe '.async_refresh' do
+ it 'schedules the worker' do
+ expect(::GoogleCloud::FetchGoogleIpListWorker).to receive(:perform_async)
+
+ described_class.async_refresh
+ end
+ end
+end
diff --git a/spec/uploaders/object_storage/cdn_spec.rb b/spec/uploaders/object_storage/cdn_spec.rb
new file mode 100644
index 00000000000..246cb1bf349
--- /dev/null
+++ b/spec/uploaders/object_storage/cdn_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ObjectStorage::CDN do
+ let(:cdn_options) do
+ {
+ 'object_store' => {
+ 'cdn' => {
+ 'provider' => 'google',
+ 'url' => 'https://gitlab.example.com',
+ 'key_name' => 'test-key',
+ 'key' => '12345'
+ }
+ }
+ }.freeze
+ end
+
+ let(:uploader_class) do
+ Class.new(GitlabUploader) do
+ include ObjectStorage::Concern
+ include ObjectStorage::CDN::Concern
+
+ private
+
+ # user/:id
+ def dynamic_segment
+ File.join(model.class.underscore, model.id.to_s)
+ end
+ end
+ end
+
+ let(:object) { build_stubbed(:user) }
+
+ subject { uploader_class.new(object, :file) }
+
+ context 'with CDN config' do
+ before do
+ uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
+ end
+
+ describe '#use_cdn?' do
+ it 'returns true' do
+ expect_next_instance_of(ObjectStorage::CDN::GoogleCDN) do |cdn|
+ expect(cdn).to receive(:use_cdn?).and_return(true)
+ end
+
+ expect(subject.use_cdn?('18.245.0.1')).to be true
+ end
+ end
+
+ describe '#cdn_signed_url' do
+ it 'returns a URL' do
+ expect_next_instance_of(ObjectStorage::CDN::GoogleCDN) do |cdn|
+ expect(cdn).to receive(:signed_url).and_return("https://cdn.example.com/path")
+ end
+
+ expect(subject.cdn_signed_url).to eq("https://cdn.example.com/path")
+ end
+ end
+ end
+
+ context 'without CDN config' do
+ before do
+ uploader_class.options = Gitlab.config.uploads
+ end
+
+ describe '#use_cdn?' do
+ it 'returns false' do
+ expect(subject.use_cdn?('18.245.0.1')).to be false
+ end
+ end
+ end
+
+ context 'with an unknown CDN provider' do
+ before do
+ cdn_options['object_store']['cdn']['provider'] = 'amazon'
+ uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
+ end
+
+ it 'raises an error' do
+ expect { subject.use_cdn?('18.245.0.1') }.to raise_error("Unknown CDN provider: amazon")
+ end
+ end
+end
diff --git a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
index 203a453bcdd..dbbf69e3c8d 100644
--- a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
@@ -49,12 +49,12 @@ RSpec.describe Packages::Debian::DistributionReleaseFileUploader do
end
describe '#filename' do
- it { expect(subject.filename).to eq('Release')}
+ it { expect(subject.filename).to eq('Release') }
context 'with signed_file' do
let(:uploader) { described_class.new(distribution, :signed_file) }
- it { expect(subject.filename).to eq('InRelease')}
+ it { expect(subject.filename).to eq('InRelease') }
end
end
end
diff --git a/spec/uploaders/packages/package_file_uploader_spec.rb b/spec/uploaders/packages/package_file_uploader_spec.rb
index e8f4cae7b04..0c7bf6432cb 100644
--- a/spec/uploaders/packages/package_file_uploader_spec.rb
+++ b/spec/uploaders/packages/package_file_uploader_spec.rb
@@ -2,50 +2,43 @@
require 'spec_helper'
RSpec.describe Packages::PackageFileUploader do
- {
- package_file: %r[^\h{2}/\h{2}/\h{64}/packages/\d+/files/\d+$],
- debian_package_file: %r[^\h{2}/\h{2}/\h{64}/packages/debian/files/\d+$]
- }.each do |factory, store_dir_regex|
- context factory.to_s do
- let(:package_file) { create(factory) } # rubocop:disable Rails/SaveBang
- let(:uploader) { described_class.new(package_file, :file) }
- let(:path) { Gitlab.config.packages.storage_path }
-
- subject { uploader }
-
- it_behaves_like "builds correct paths",
- store_dir: store_dir_regex,
- cache_dir: %r[/packages/tmp/cache],
- work_dir: %r[/packages/tmp/work]
-
- context 'object store is remote' do
- before do
- stub_package_file_object_storage
- end
-
- include_context 'with storage', described_class::Store::REMOTE
-
- it_behaves_like "builds correct paths",
- store_dir: store_dir_regex
- end
+ let(:package_file) { create(:package_file) }
+ let(:uploader) { described_class.new(package_file, :file) }
+ let(:path) { Gitlab.config.packages.storage_path }
+
+ subject { uploader }
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/\d+/files/\d+$],
+ cache_dir: %r[/packages/tmp/cache],
+ work_dir: %r[/packages/tmp/work]
+
+ context 'object store is remote' do
+ before do
+ stub_package_file_object_storage
+ end
- describe 'remote file' do
- let(:package_file) { create(factory, :object_storage) }
+ include_context 'with storage', described_class::Store::REMOTE
- context 'with object storage enabled' do
- before do
- stub_package_file_object_storage
- end
+ it_behaves_like "builds correct paths",
+ store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/\d+/files/\d+$]
+ end
+
+ describe 'remote file' do
+ let(:package_file) { create(:package_file, :object_storage) }
+
+ context 'with object storage enabled' do
+ before do
+ stub_package_file_object_storage
+ end
- it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
+ it 'can store file remotely' do
+ allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
- package_file
+ package_file
- expect(package_file.file_store).to eq(described_class::Store::REMOTE)
- expect(package_file.file.path).not_to be_blank
- end
- end
+ expect(package_file.file_store).to eq(described_class::Store::REMOTE)
+ expect(package_file.file.path).not_to be_blank
end
end
end
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
index fd01a18e810..1746f480c9b 100644
--- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -3,120 +3,62 @@
require 'spec_helper'
RSpec.describe ObjectStorage::MigrateUploadsWorker do
- let(:model_class) { Project }
+ let(:project) { create(:project, :with_avatar) }
let(:uploads) { Upload.all }
- let(:to_store) { ObjectStorage::Store::REMOTE }
- def perform(uploads, store = nil)
- described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, store || to_store)
+ def perform(uploads, store = ObjectStorage::Store::REMOTE)
+ described_class.new.perform(uploads.ids, store)
rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
# swallow
end
- # Expects the calling spec to define:
- # - model_class
- # - mounted_as
- # - to_store
- RSpec.shared_examples 'uploads migration worker' do
- describe '.enqueue!' do
- def enqueue!
- described_class.enqueue!(uploads, model_class, mounted_as, to_store)
- end
-
- it 'is guarded by .sanity_check!' do
- expect(described_class).to receive(:perform_async)
- expect(described_class).to receive(:sanity_check!)
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+ stub_uploads_object_storage(FileUploader)
- enqueue!
- end
+ FileUploader.new(project).store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
- context 'sanity_check! fails' do
- before do
- expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError)
- end
+ describe '#perform' do
+ it 'migrates files to remote storage' do
+ expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated 2/2 files})
- it 'does not enqueue a job' do
- expect(described_class).not_to receive(:perform_async)
+ perform(uploads)
- expect { enqueue! }.to raise_error(described_class::SanityCheckError)
- end
- end
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
+ expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(2)
end
- describe '.sanity_check!' do
- shared_examples 'raises a SanityCheckError' do |expected_message|
- let(:mount_point) { nil }
-
- it do
- expect { described_class.sanity_check!(uploads, model_class, mount_point) }
- .to raise_error(described_class::SanityCheckError).with_message(expected_message)
- end
+ context 'reversed' do
+ before do
+ perform(uploads)
end
- context 'uploader types mismatch' do
- let!(:outlier) { create(:upload, uploader: 'GitlabUploader') }
+ it 'migrates files to local storage' do
+ expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(2)
- include_examples 'raises a SanityCheckError', /Multiple uploaders found/
- end
+ perform(uploads, ObjectStorage::Store::LOCAL)
- context 'mount point not found' do
- include_examples 'raises a SanityCheckError', /Mount point [a-z:]+ not found in/ do
- let(:mount_point) { :potato }
- end
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(2)
+ expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(0)
end
end
- describe '#perform' do
- it 'migrates files to remote storage' do
- expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated 1/1 files})
-
- perform(uploads)
-
- expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
- end
-
- context 'reversed' do
- let(:to_store) { ObjectStorage::Store::LOCAL }
-
- before do
- perform(uploads, ObjectStorage::Store::REMOTE)
- end
-
- it 'migrates files to local storage' do
- expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(1)
-
- perform(uploads)
-
- expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(1)
- end
+ context 'migration is unsuccessful' do
+ before do
+ allow_any_instance_of(ObjectStorage::Concern)
+ .to receive(:migrate!).and_raise(CarrierWave::UploadError, 'I am a teapot.')
end
- context 'migration is unsuccessful' do
- before do
- allow_any_instance_of(ObjectStorage::Concern)
- .to receive(:migrate!).and_raise(CarrierWave::UploadError, 'I am a teapot.')
- end
-
- it 'does not migrate files to remote storage' do
- expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/)
+ it 'does not migrate files to remote storage' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/)
- perform(uploads)
+ perform(uploads)
- expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(1)
- end
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(2)
+ expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(0)
end
end
- end
-
- context "for AvatarUploader" do
- let!(:project_with_avatar) { create(:project, :with_avatar) }
- let(:mounted_as) { :avatar }
-
- before do
- stub_uploads_object_storage(AvatarUploader)
- end
-
- it_behaves_like "uploads migration worker"
describe "limits N+1 queries" do
it "to N*5" do
@@ -127,46 +69,18 @@ RSpec.describe ObjectStorage::MigrateUploadsWorker do
expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(5)
end
end
- end
-
- context "for FileUploader" do
- let!(:project_with_file) { create(:project) }
- let(:secret) { SecureRandom.hex }
- let(:mounted_as) { nil }
-
- def upload_file(project)
- uploader = FileUploader.new(project)
- uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
- end
-
- before do
- stub_uploads_object_storage(FileUploader)
-
- upload_file(project_with_file)
- end
-
- it_behaves_like "uploads migration worker"
-
- describe "limits N+1 queries" do
- it "to N*5" do
- query_count = ActiveRecord::QueryRecorder.new { perform(uploads) }
- upload_file(create(:project))
+ it 'handles legacy argument format' do
+ described_class.new.perform(uploads.ids, 'Project', :avatar, ObjectStorage::Store::REMOTE)
- expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(5)
- end
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
+ expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(2)
end
- end
- context 'for DesignManagement::DesignV432x230Uploader' do
- let(:model_class) { DesignManagement::Action }
- let!(:design_action) { create(:design_action, :with_image_v432x230) }
- let(:mounted_as) { :image_v432x230 }
+ it 'logs an error when number of arguments is incorrect' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Job has wrong arguments format/)
- before do
- stub_uploads_object_storage(DesignManagement::DesignV432x230Uploader)
+ described_class.new.perform(uploads.ids, 'Project', ObjectStorage::Store::REMOTE)
end
-
- it_behaves_like 'uploads migration worker'
end
end
diff --git a/spec/validators/addressable_url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index b3a4459db30..9109a899881 100644
--- a/spec/validators/addressable_url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -245,7 +245,7 @@ RSpec.describe AddressableUrlValidator do
end
context 'when enforce_user is' do
- let(:url) { 'http://$user@example.com'}
+ let(:url) { 'http://$user@example.com' }
let(:validator) { described_class.new(attributes: [:link_url], enforce_user: enforce_user) }
context 'true' do
@@ -274,7 +274,7 @@ RSpec.describe AddressableUrlValidator do
end
context 'when ascii_only is' do
- let(:url) { 'https://𝕘itⅼαƄ.com/foo/foo.bar'}
+ let(:url) { 'https://𝕘itⅼαƄ.com/foo/foo.bar' }
let(:validator) { described_class.new(attributes: [:link_url], ascii_only: ascii_only) }
context 'true' do
diff --git a/spec/views/admin/sessions/new.html.haml_spec.rb b/spec/views/admin/sessions/new.html.haml_spec.rb
index 97528b6e782..ac35bbef5b4 100644
--- a/spec/views/admin/sessions/new.html.haml_spec.rb
+++ b/spec/views/admin/sessions/new.html.haml_spec.rb
@@ -19,9 +19,9 @@ RSpec.describe 'admin/sessions/new.html.haml' do
it 'shows enter password form' do
render
- expect(rendered).to have_selector('[data-qa-selector="sign_in_tab"]') # rubocop:disable QA/SelectorUsage
+ expect(rendered).to have_selector('[data-testid="sign-in-tab"]')
expect(rendered).to have_css('#login-pane.active')
- expect(rendered).to have_selector('[data-qa-selector="password_field"]') # rubocop:disable QA/SelectorUsage
+ expect(rendered).to have_selector('[data-testid="password-field"]')
end
it 'warns authentication not possible if password not set' do
@@ -60,7 +60,7 @@ RSpec.describe 'admin/sessions/new.html.haml' do
it 'is shown when enabled' do
render
- expect(rendered).to have_selector('[data-qa-selector="ldap_tab"]') # rubocop:disable QA/SelectorUsage
+ expect(rendered).to have_selector('[data-testid="ldap-tab"]')
expect(rendered).to have_css('.login-box#ldapmain')
expect(rendered).to have_field('LDAP Username')
expect(rendered).not_to have_content('No authentication methods configured')
@@ -71,7 +71,7 @@ RSpec.describe 'admin/sessions/new.html.haml' do
render
- expect(rendered).not_to have_selector('[data-qa-selector="ldap_tab"]') # rubocop:disable QA/SelectorUsage
+ expect(rendered).not_to have_selector('[data-testid="ldap-tab"]')
expect(rendered).not_to have_field('LDAP Username')
expect(rendered).to have_content('No authentication methods configured')
end
diff --git a/spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb b/spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb
index edec46ad0a3..6f6596caabb 100644
--- a/spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb
+++ b/spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb
@@ -3,15 +3,65 @@
require 'spec_helper'
RSpec.describe 'dashboard/projects/_blank_state_welcome.html.haml' do
- let_it_be(:user) { create(:user) }
+ context 'with regular user' do
+ context 'with project creation enabled' do
+ let_it_be(:user) { create(:user) }
- before do
- allow(view).to receive(:current_user).and_return(user)
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ it 'has a doc_url' do
+ render
+
+ expect(rendered).to have_link(href: Gitlab::Saas.doc_url)
+ end
+
+ it "shows create project panel" do
+ render
+
+ expect(rendered).to include(_('Create a project'))
+ end
+ end
+
+ context 'with project creation disabled' do
+ let_it_be(:user_projects_limit) { create(:user, projects_limit: 0 ) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user_projects_limit)
+ end
+
+ it "doesn't show create project panel" do
+ render
+
+ expect(rendered).not_to include(_('Create a project'))
+ end
+
+ it 'shows an alert' do
+ render
+
+ expect(rendered).to include(_("You see projects here when you're added to a group or project."))
+ end
+ end
end
- it 'has a doc_url' do
- render
+ context 'with external user' do
+ let_it_be(:external_user) { create(:user, :external) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(external_user)
+ end
+
+ it "doesn't show create project panel" do
+ render
+
+ expect(rendered).not_to include(_('Create a project'))
+ end
+
+ it 'shows an alert' do
+ render
- expect(rendered).to have_link(href: Gitlab::Saas.doc_url)
+ expect(rendered).to include(_("You see projects here when you're added to a group or project."))
+ end
end
end
diff --git a/spec/views/devise/sessions/new.html.haml_spec.rb b/spec/views/devise/sessions/new.html.haml_spec.rb
index c8e9aa15287..798c891e75c 100644
--- a/spec/views/devise/sessions/new.html.haml_spec.rb
+++ b/spec/views/devise/sessions/new.html.haml_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'devise/sessions/new' do
render
expect(rendered).to have_selector('.new-session-tabs')
- expect(rendered).to have_selector('[data-qa-selector="ldap_tab"]') # rubocop:disable QA/SelectorUsage
+ expect(rendered).to have_selector('[data-testid="ldap-tab"]')
expect(rendered).to have_field('LDAP Username')
end
@@ -65,7 +65,7 @@ RSpec.describe 'devise/sessions/new' do
render
expect(rendered).to have_content('No authentication methods configured')
- expect(rendered).not_to have_selector('[data-qa-selector="ldap_tab"]') # rubocop:disable QA/SelectorUsage
+ expect(rendered).not_to have_selector('[data-testid="ldap-tab"]')
expect(rendered).not_to have_field('LDAP Username')
end
end
diff --git a/spec/views/devise/shared/_signup_box.html.haml_spec.rb b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
index b0730e6fc54..ee9ccbf6ff5 100644
--- a/spec/views/devise/shared/_signup_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
@@ -7,13 +7,15 @@ RSpec.describe 'devise/shared/_signup_box' do
let(:terms_path) { '_terms_path_' }
let(:translation_com) do
- s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted "\
- "the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")
+ s_("SignUp|By clicking %{button_text} or registering through a third party you "\
+ "accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy "\
+ "and Cookie Policy%{link_end}")
end
let(:translation_non_com) do
- s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted "\
- "the %{link_start}Terms of Use and Privacy Policy%{link_end}")
+ s_("SignUp|By clicking %{button_text} or registering through a third party you "\
+ "accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and "\
+ "Cookie Policy%{link_end}")
end
before do
diff --git a/spec/views/groups/new.html.haml_spec.rb b/spec/views/groups/new.html.haml_spec.rb
index 8b12cc42a88..5c7378e8dc7 100644
--- a/spec/views/groups/new.html.haml_spec.rb
+++ b/spec/views/groups/new.html.haml_spec.rb
@@ -25,4 +25,15 @@ RSpec.describe 'groups/new.html.haml' do
expect(rendered).not_to have_checked_field('Just me')
end
end
+
+ context 'when a subgroup' do
+ let_it_be(:group) { create(:group, :nested) }
+
+ it 'renders the visibility level section' do
+ expect(rendered).to have_content('Visibility level')
+ expect(rendered).to have_field('Private')
+ expect(rendered).to have_field('Internal')
+ expect(rendered).to have_field('Public')
+ end
+ end
end
diff --git a/spec/views/groups/observability.html.haml_spec.rb b/spec/views/groups/observability.html.haml_spec.rb
new file mode 100644
index 00000000000..db280d5a2ba
--- /dev/null
+++ b/spec/views/groups/observability.html.haml_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'groups/observability/index' do
+ let_it_be(:iframe_url) { "foo.test" }
+
+ before do
+ assign(:observability_iframe_src, iframe_url)
+ end
+
+ it 'renders as expected' do
+ render
+ page = Capybara.string(rendered)
+ iframe = page.find('iframe#observability-ui-iframe')
+ expect(iframe['src']).to eq(iframe_url)
+ end
+end
diff --git a/spec/views/help/drawers.html.haml_spec.rb b/spec/views/help/drawers.html.haml_spec.rb
new file mode 100644
index 00000000000..b250d00bb20
--- /dev/null
+++ b/spec/views/help/drawers.html.haml_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'help/drawers' do
+ describe 'Markdown rendering' do
+ before do
+ allow(view).to receive(:get_markdown_without_frontmatter).and_return('[GitLab](https://about.gitlab.com/)')
+ assign(:clean_path, 'user/ssh')
+ end
+
+ it 'renders Markdown' do
+ render
+
+ expect(rendered).to have_link('GitLab', href: 'https://about.gitlab.com/')
+ end
+ end
+end
diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb
index fbf84a5d272..4461eadf1a3 100644
--- a/spec/views/help/instance_configuration.html.haml_spec.rb
+++ b/spec/views/help/instance_configuration.html.haml_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'help/instance_configuration' do
describe 'General Sections:' do
- let(:instance_configuration) { build(:instance_configuration)}
+ let(:instance_configuration) { build(:instance_configuration) }
let(:settings) { instance_configuration.settings }
let(:ssh_settings) { settings[:ssh_algorithms_hashes] }
diff --git a/spec/views/layouts/_header_search.html.haml_spec.rb b/spec/views/layouts/_header_search.html.haml_spec.rb
index 3ab4ae6a483..3a21bb3a92c 100644
--- a/spec/views/layouts/_header_search.html.haml_spec.rb
+++ b/spec/views/layouts/_header_search.html.haml_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'layouts/_header_search' do
let(:scope) { nil }
let(:ref) { nil }
let(:code_search) { false }
- let(:for_snippets) { false}
+ let(:for_snippets) { false }
let(:header_search_context) do
{
diff --git a/spec/views/layouts/_published_experiments.html.haml_spec.rb b/spec/views/layouts/_published_experiments.html.haml_spec.rb
index 84894554bd9..072e4f2074e 100644
--- a/spec/views/layouts/_published_experiments.html.haml_spec.rb
+++ b/spec/views/layouts/_published_experiments.html.haml_spec.rb
@@ -13,10 +13,10 @@ RSpec.describe 'layouts/_published_experiments', :experiment do
test_variant: :variant_name
)
- experiment(:test_control) { }
+ experiment(:test_control) {}
experiment(:test_excluded) { |e| e.exclude! }
- experiment(:test_candidate) { |e| e.candidate { } }
- experiment(:test_variant) { |e| e.variant(:variant_name) { } }
+ experiment(:test_candidate) { |e| e.candidate {} }
+ experiment(:test_variant) { |e| e.variant(:variant_name) {} }
experiment(:test_published_only).publish
render
diff --git a/spec/views/layouts/fullscreen.html.haml_spec.rb b/spec/views/layouts/fullscreen.html.haml_spec.rb
index 0ae2c76ebcb..14b382bc238 100644
--- a/spec/views/layouts/fullscreen.html.haml_spec.rb
+++ b/spec/views/layouts/fullscreen.html.haml_spec.rb
@@ -9,5 +9,46 @@ RSpec.describe 'layouts/fullscreen' do
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
end
+ it 'renders a flex container' do
+ render
+
+ expect(rendered).to have_selector(".gl--flex-full.gl-h-full")
+ expect(rendered).to have_selector(".gl--flex-full.gl-w-full")
+ end
+
it_behaves_like 'a layout which reflects the application theme setting'
+
+ describe 'sidebar' do
+ context 'when nav is set' do
+ before do
+ allow(view).to receive(:nav).and_return("admin")
+ render
+ end
+
+ it 'renders the sidebar' do
+ expect(rendered).to render_template("layouts/nav/sidebar/_admin")
+ expect(rendered).to have_selector("aside.nav-sidebar")
+ end
+
+ it 'adds the proper classes' do
+ expect(rendered).to have_selector(".layout-page.gl-mt-0\\!")
+ end
+ end
+
+ describe 'when nav is not set' do
+ before do
+ allow(view).to receive(:nav).and_return(nil)
+ render
+ end
+
+ it 'does not render the sidebar' do
+ expect(rendered).not_to render_template("layouts/nav/sidebar/_admin")
+ expect(rendered).not_to have_selector("aside.nav-sidebar")
+ end
+
+ it 'not add classes' do
+ expect(rendered).not_to have_selector(".layout-page.gl-mt-0\\!")
+ end
+ end
+ end
end
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index 79c22871b44..17251049c57 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -166,7 +166,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
let(:user) { create(:user, :external) }
it 'is nil' do
- # We have to us `view.render` because `render` causes issues
+ # We have to use `view.render` because `render` causes issues
# https://github.com/rails/rails/issues/41320
expect(view.render("layouts/header/new_dropdown")).to be_nil
end
diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
index 428e9cc8490..472a2f3cb34 100644
--- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
end
end
- describe 'Packages & Registries' do
+ describe 'Packages and registries' do
it 'has a link to the package registry page' do
stub_config(packages: { enabled: true })
@@ -178,10 +178,10 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
expect(rendered).to have_link('Applications', href: group_settings_applications_path(group))
end
- it 'has a link to the Package & Registries settings page' do
+ it 'has a link to the Package and registry settings page' do
render
- expect(rendered).to have_link('Packages & Registries', href: group_settings_packages_and_registries_path(group))
+ expect(rendered).to have_link('Packages and registries', href: group_settings_packages_and_registries_path(group))
end
end
end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 9ae3f814679..e7d9a8a4708 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -70,8 +70,8 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
describe 'Learn GitLab' do
it 'has a link to the learn GitLab' do
allow(view).to receive(:learn_gitlab_enabled?).and_return(true)
- allow_next_instance_of(LearnGitlab::Onboarding) do |onboarding|
- expect(onboarding).to receive(:completed_percentage).and_return(20)
+ allow_next_instance_of(Onboarding::Completion) do |onboarding|
+ expect(onboarding).to receive(:percentage).and_return(20)
end
render
@@ -559,7 +559,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'top level navigation link is visible and points to package registry page' do
render
- expect(rendered).to have_link('Packages & Registries', href: project_packages_path(project))
+ expect(rendered).to have_link('Packages and registries', href: project_packages_path(project))
end
describe 'Packages Registry' do
@@ -908,7 +908,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'Packages & Registries' do
+ describe 'Packages and registries' do
let(:packages_enabled) { false }
before do
@@ -919,20 +919,20 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
context 'when registry is enabled' do
let(:registry_enabled) { true }
- it 'has a link to the Packages & Registries settings' do
+ it 'has a link to the Package and registry settings' do
render
- expect(rendered).to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
+ expect(rendered).to have_link('Packages and registries', href: project_settings_packages_and_registries_path(project))
end
end
context 'when registry is not enabled' do
let(:registry_enabled) { false }
- it 'does not have a link to the Packages & Registries settings' do
+ it 'does not have a link to the Package and registry settings' do
render
- expect(rendered).not_to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
+ expect(rendered).not_to have_link('Packages and registries', href: project_settings_packages_and_registries_path(project))
end
end
@@ -940,10 +940,10 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
let(:registry_enabled) { false }
let(:packages_enabled) { true }
- it 'has a link to the Packages & Registries settings' do
+ it 'has a link to the Package and registry settings' do
render
- expect(rendered).to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
+ expect(rendered).to have_link('Packages and registries', href: project_settings_packages_and_registries_path(project))
end
end
end
diff --git a/spec/views/notify/approved_merge_request_email.html.haml_spec.rb b/spec/views/notify/approved_merge_request_email.html.haml_spec.rb
new file mode 100644
index 00000000000..7d19e628eb8
--- /dev/null
+++ b/spec/views/notify/approved_merge_request_email.html.haml_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+RSpec.describe 'notify/approved_merge_request_email.html.haml' do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ before do
+ allow(view).to receive(:message) { instance_double(Mail::Message, subject: 'Subject') }
+ assign(:project, project)
+ assign(:approved_by, user)
+ assign(:merge_request, merge_request)
+ end
+
+ it 'contains approval information' do
+ render
+
+ expect(rendered).to have_content(merge_request.to_reference.to_s)
+ expect(rendered).to have_content("was approved by")
+ expect(rendered).to have_content(user.name.to_s)
+ end
+end
diff --git a/spec/views/notify/autodevops_disabled_email.text.erb_spec.rb b/spec/views/notify/autodevops_disabled_email.text.erb_spec.rb
index c3cb0c83f35..d8299d637e1 100644
--- a/spec/views/notify/autodevops_disabled_email.text.erb_spec.rb
+++ b/spec/views/notify/autodevops_disabled_email.text.erb_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'notify/autodevops_disabled_email.text.erb' do
expect(rendered).to have_content("Auto DevOps pipeline was disabled for #{project.name}")
expect(rendered).to match(/Pipeline ##{pipeline.id} .* triggered by #{pipeline.user.name}/)
- expect(rendered).to have_content("Stage: #{build.stage}")
+ expect(rendered).to have_content("Stage: #{build.stage_name}")
expect(rendered).to have_content("Name: #{build.name}")
expect(rendered).not_to have_content("Trace:")
end
diff --git a/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb b/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb
index 6d56145144f..deef13fec99 100644
--- a/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb
+++ b/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb
@@ -12,10 +12,25 @@ RSpec.describe 'notify/change_in_merge_request_draft_status_email.html.haml' do
assign(:merge_request, merge_request)
end
+ it 'shows user added draft status on email' do
+ merge_request.update!(title: merge_request.draft_title)
+
+ render
+
+ expect(merge_request.draft).to be_truthy
+ expect(rendered).to have_content("#{user.name} marked merge request #{merge_request.to_reference} as draft")
+ end
+
+ it 'shows user removed draft status on email' do
+ render
+
+ expect(merge_request.draft).to be_falsy
+ expect(rendered).to have_content("#{user.name} marked merge request #{merge_request.to_reference} as ready")
+ end
+
it 'renders the email correctly' do
render
- expect(rendered).to have_content("#{user.name} changed the draft status of merge request #{merge_request.to_reference}")
expect(rendered).to have_link(user.name, href: user_url(user))
expect(rendered).to have_link(merge_request.to_reference, href: merge_request_link)
end
diff --git a/spec/views/notify/change_in_merge_request_draft_status_email.text.erb_spec.rb b/spec/views/notify/change_in_merge_request_draft_status_email.text.erb_spec.rb
index a05c20fd8c4..3faba483516 100644
--- a/spec/views/notify/change_in_merge_request_draft_status_email.text.erb_spec.rb
+++ b/spec/views/notify/change_in_merge_request_draft_status_email.text.erb_spec.rb
@@ -12,9 +12,19 @@ RSpec.describe 'notify/change_in_merge_request_draft_status_email.text.erb' do
it_behaves_like 'renders plain text email correctly'
- it 'renders the email correctly' do
+ it 'shows user added draft status on email' do
+ merge_request.update!(title: merge_request.draft_title)
+
+ render
+
+ expect(merge_request.draft).to be_truthy
+ expect(rendered).to have_content("#{user.name} marked merge request #{merge_request.to_reference} as draft")
+ end
+
+ it 'shows user removed draft status on email' do
render
- expect(rendered).to have_content("#{user.name} changed the draft status of merge request #{merge_request.to_reference}")
+ expect(merge_request.draft).to be_falsy
+ expect(rendered).to have_content("#{user.name} marked merge request #{merge_request.to_reference} as ready")
end
end
diff --git a/spec/views/notify/import_issues_csv_email.html.haml_spec.rb b/spec/views/notify/import_issues_csv_email.html.haml_spec.rb
new file mode 100644
index 00000000000..43dfab87ac9
--- /dev/null
+++ b/spec/views/notify/import_issues_csv_email.html.haml_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'notify/import_issues_csv_email.html.haml' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:correct_results) { { success: 3, valid_file: true } }
+ let(:errored_results) { { success: 3, error_lines: [5, 6, 7], valid_file: true } }
+ let(:parse_error_results) { { success: 0, parse_error: true } }
+
+ before do
+ assign(:user, user)
+ assign(:project, project)
+ end
+
+ context 'when no errors found while importing' do
+ before do
+ assign(:results, correct_results)
+ end
+
+ it 'renders correctly' do
+ render
+
+ expect(rendered).to have_link(project.full_name, href: project_url(project))
+ expect(rendered).to have_content("3 issues imported")
+ expect(rendered).not_to have_content("Errors found on line")
+ expect(rendered).not_to have_content(
+ "Error parsing CSV file. Please make sure it has the correct format: \
+a delimited text file that uses a comma to separate values.")
+ end
+ end
+
+ context 'when import errors reported' do
+ before do
+ assign(:results, errored_results)
+ end
+
+ it 'renders correctly' do
+ render
+
+ expect(rendered).to have_content("Errors found on lines: #{errored_results[:error_lines].join(", ")}. \
+Please check if these lines have an issue title.")
+ expect(rendered).not_to have_content("Error parsing CSV file. Please make sure it has the correct format: \
+a delimited text file that uses a comma to separate values.")
+ end
+ end
+
+ context 'when parse error reported while importing' do
+ before do
+ assign(:results, parse_error_results)
+ end
+
+ it 'renders with parse error' do
+ render
+
+ expect(rendered).to have_content("Error parsing CSV file. \
+Please make sure it has the correct format: a delimited text file that uses a comma to separate values.")
+ end
+ end
+end
diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb
index c807512a11a..3c61afb21c5 100644
--- a/spec/views/profiles/keys/_form.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_form.html.haml_spec.rb
@@ -33,9 +33,9 @@ RSpec.describe 'profiles/keys/_form.html.haml' do
end
it 'has the expires at field', :aggregate_failures do
- expect(rendered).to have_field('Expiration date', type: 'date')
+ expect(rendered).to have_field('Expiration date', type: 'text')
expect(page.find_field('Expiration date')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
- expect(rendered).to have_text('Key becomes invalid on this date')
+ expect(rendered).to have_text( s_('Profiles|Optional but recommended. If set, key becomes invalid on the specified date.'))
end
it 'has the validation warning', :aggregate_failures do
diff --git a/spec/views/profiles/preferences/show.html.haml_spec.rb b/spec/views/profiles/preferences/show.html.haml_spec.rb
index 2fe941b9f14..4e4499c3252 100644
--- a/spec/views/profiles/preferences/show.html.haml_spec.rb
+++ b/spec/views/profiles/preferences/show.html.haml_spec.rb
@@ -54,8 +54,8 @@ RSpec.describe 'profiles/preferences/show' do
end
it 'has helpful homepage setup guidance' do
- expect(rendered).to have_field('Homepage content')
- expect(rendered).to have_content('Choose what content you want to see on your homepage.')
+ expect(rendered).to have_field('Dashboard')
+ expect(rendered).to have_content('Choose what content you want to see by default on your dashboard.')
end
end
diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb
index daa1d20e6b1..5751d47ee97 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/profiles/show.html.haml_spec.rb
@@ -17,6 +17,11 @@ RSpec.describe 'profiles/show' do
expect(rendered).to have_field('user_name', with: user.name)
expect(rendered).to have_field('user_id', with: user.id)
+
+ expectd_link = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits')
+ expected_link_html = "<a href=\"#{expectd_link}\" target=\"_blank\" " \
+ "rel=\"noopener noreferrer\">#{_('Learn more.')}</a>"
+ expect(rendered.include?(expected_link_html)).to eq(true)
end
end
end
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index a85ddf7a005..2935e4395ba 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -28,62 +28,6 @@ RSpec.describe 'projects/edit' do
end
end
- context 'merge suggestions settings' do
- it 'displays a placeholder if none is set' do
- render
-
- expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: "Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)")
- end
-
- it 'displays the user entered value' do
- project.update!(suggestion_commit_message: 'refactor: changed %{file_paths}')
-
- render
-
- expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_paths}')
- end
- end
-
- context 'merge commit template' do
- it 'displays default template if none is set' do
- render
-
- expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip)
- Merge branch '%{source_branch}' into '%{target_branch}'
-
- %{title}
-
- %{issues}
-
- See merge request %{reference}
- MSG
- end
-
- it 'displays the user entered value' do
- project.update!(merge_commit_template: '%{title}')
-
- render
-
- expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}')
- end
- end
-
- context 'squash template' do
- it 'displays default template if none is set' do
- render
-
- expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}')
- end
-
- it 'displays the user entered value' do
- project.update!(squash_commit_template: '%{first_multiline_commit}')
-
- render
-
- expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}')
- end
- end
-
context 'forking' do
before do
assign(:project, project)
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
index 7c171ee65b9..7f537022445 100644
--- a/spec/views/projects/imports/new.html.haml_spec.rb
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe "projects/imports/new.html.haml" do
project.add_maintainer(user)
end
- it "escapes HTML in import errors" do
+ it "escapes HTML in import errors", :skip_html_escaped_tags_check do
assign(:project, project)
render
diff --git a/spec/views/projects/settings/merge_requests/show.html.haml_spec.rb b/spec/views/projects/settings/merge_requests/show.html.haml_spec.rb
new file mode 100644
index 00000000000..821f430eb10
--- /dev/null
+++ b/spec/views/projects/settings/merge_requests/show.html.haml_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/settings/merge_requests/show' do
+ include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
+
+ let(:project) { create(:project) }
+ let(:user) { create(:admin) }
+
+ before do
+ assign(:project, project)
+
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
+ end
+
+ describe 'merge suggestions settings' do
+ it 'displays a placeholder if none is set' do
+ render
+
+ placeholder = "Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)"
+
+ expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: placeholder)
+ end
+
+ it 'displays the user entered value' do
+ project.update!(suggestion_commit_message: 'refactor: changed %{file_paths}')
+
+ render
+
+ expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_paths}')
+ end
+ end
+
+ describe 'merge commit template' do
+ it 'displays default template if none is set' do
+ render
+
+ expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip)
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{title}
+
+ %{issues}
+
+ See merge request %{reference}
+ MSG
+ end
+
+ it 'displays the user entered value' do
+ project.update!(merge_commit_template: '%{title}')
+
+ render
+
+ expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}')
+ end
+ end
+
+ describe 'squash template' do
+ it 'displays default template if none is set' do
+ render
+
+ expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}')
+ end
+
+ it 'displays the user entered value' do
+ project.update!(squash_commit_template: '%{first_multiline_commit}')
+
+ render
+
+ expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}')
+ end
+ end
+end
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index aff233b697f..99db5d9e2a8 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe 'projects/tags/index.html.haml' do
let_it_be(:git_tag) { project.repository.tags.last }
let_it_be(:release) do
create(:release, project: project,
- sha: git_tag.target_commit.sha,
- tag: 'v1.1.0')
+ sha: git_tag.target_commit.sha,
+ tag: 'v1.1.0')
end
let(:pipeline) { create(:ci_pipeline, :success, project: project, ref: git_tag.name, sha: release.sha) }
diff --git a/spec/views/shared/runners/_runner_details.html.haml_spec.rb b/spec/views/shared/runners/_runner_details.html.haml_spec.rb
index cdf5ec563d0..978750c8435 100644
--- a/spec/views/shared/runners/_runner_details.html.haml_spec.rb
+++ b/spec/views/shared/runners/_runner_details.html.haml_spec.rb
@@ -113,14 +113,14 @@ RSpec.describe 'shared/runners/_runner_details.html.haml' do
describe 'Tags value' do
context 'when runner does not have tags' do
it { is_expected.to have_content('Tags') }
- it { is_expected.not_to have_selector('span.gl-badge.badge.badge-info')}
+ it { is_expected.not_to have_selector('span.gl-badge.badge.badge-info') }
end
context 'when runner have tags' do
let(:runner) { create(:ci_runner, tag_list: %w(tag2 tag3 tag1)) }
it { is_expected.to have_content('Tags tag1 tag2 tag3') }
- it { is_expected.to have_selector('span.gl-badge.badge.badge-info')}
+ it { is_expected.to have_selector('span.gl-badge.badge.badge-info') }
end
end
diff --git a/spec/views/shared/web_hooks/_web_hook_disabled_alert.html.haml_spec.rb b/spec/views/shared/web_hooks/_web_hook_disabled_alert.html.haml_spec.rb
new file mode 100644
index 00000000000..22ed8bb262c
--- /dev/null
+++ b/spec/views/shared/web_hooks/_web_hook_disabled_alert.html.haml_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'shared/web_hooks/_web_hook_disabled_alert' do
+ let_it_be(:project) { create(:project) }
+
+ let(:show_project_hook_failed_callout?) { false }
+
+ def after_flash_content
+ view.content_for(:after_flash_content)
+ end
+
+ before do
+ assign(:project, project)
+ allow(view).to receive(:show_project_hook_failed_callout?).and_return(show_project_hook_failed_callout?)
+ end
+
+ context 'when show_project_hook_failed_callout? is true' do
+ let(:show_project_hook_failed_callout?) { true }
+
+ it 'adds alert to `:after_flash_content`' do
+ render
+
+ expect(after_flash_content).to have_content('Webhook disabled')
+ end
+ end
+
+ context 'when show_project_hook_failed_callout? is false' do
+ it 'does not add alert to `:after_flash_content`' do
+ # We have to use `view.render` because `render` causes issues
+ # https://github.com/rails/rails/issues/41320
+ view.render('shared/web_hooks/web_hook_disabled_alert')
+
+ expect(after_flash_content).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb b/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
index c45ec20fe5a..ee1bbafa9b5 100644
--- a/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
+++ b/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
@@ -48,11 +48,43 @@ RSpec.describe Analytics::UsageTrends::CounterJobWorker do
end
it 'does not insert anything when BatchCount returns error' do
- allow(Gitlab::Database::BatchCount).to receive(:batch_count).and_return(Gitlab::Database::BatchCounter::FALLBACK)
+ allow(Gitlab::Database::BatchCount).to receive(:batch_count_with_timeout)
+ .and_return({ status: :canceled })
expect { subject }.not_to change { Analytics::UsageTrends::Measurement.count }
end
+ context 'when the timeout elapses' do
+ let(:min_id) { 1 }
+ let(:max_id) { 12345 }
+ let(:continue_from) { 321 }
+ let(:partial_results) { 42 }
+ let(:final_count) { 123 }
+
+ subject { described_class.new.perform(users_measurement_identifier, min_id, max_id, recorded_at) }
+
+ it 'continues counting later when the timeout elapses' do
+ expect(Gitlab::Database::BatchCount).to receive(:batch_count_with_timeout)
+ .with(anything, start: min_id, finish: max_id, timeout: 250.seconds, partial_results: nil)
+ .and_return({ status: :timeout, partial_results: partial_results, continue_from: continue_from })
+
+ expect(described_class).to receive(:perform_async).with(anything, continue_from, max_id, recorded_at, partial_results) do |*args|
+ described_class.new.perform(*args)
+ end
+
+ expect(Gitlab::Database::BatchCount).to receive(:batch_count_with_timeout)
+ .with(anything, start: continue_from, finish: max_id, timeout: 250.seconds, partial_results: partial_results)
+ .and_return({ status: :completed, count: final_count })
+
+ expect { subject }.to change { Analytics::UsageTrends::Measurement.count }
+
+ measurement = Analytics::UsageTrends::Measurement.users.last
+ expect(measurement.recorded_at).to be_like_time(recorded_at)
+ expect(measurement.identifier).to eq('users')
+ expect(measurement.count).to eq(final_count)
+ end
+ end
+
context 'when pipelines_succeeded identifier is passed' do
let_it_be(:pipeline) { create(:ci_pipeline, :success) }
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 846df63a4d7..a7f7aaa7dba 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
context 'when entity is group' do
let(:entity) { create(:bulk_import_entity, :group_entity, source_full_path: 'foo/bar', bulk_import: bulk_import) }
- let(:expected) { '/groups/foo%2Fbar/export_relations'}
+ let(:expected) { '/groups/foo%2Fbar/export_relations' }
include_examples 'requests relations export for api resource'
end
diff --git a/spec/workers/ci/build_finished_worker_spec.rb b/spec/workers/ci/build_finished_worker_spec.rb
index 5ddaabc3938..e8bb3988001 100644
--- a/spec/workers/ci/build_finished_worker_spec.rb
+++ b/spec/workers/ci/build_finished_worker_spec.rb
@@ -27,19 +27,6 @@ RSpec.describe Ci::BuildFinishedWorker do
subject
end
- context 'when the execute_build_hooks_inline feature flag is disabled' do
- before do
- stub_feature_flags(execute_build_hooks_inline: false)
- end
-
- it 'uses the BuildHooksWorker' do
- expect(build).not_to receive(:execute_hooks)
- expect(BuildHooksWorker).to receive(:perform_async).with(build)
-
- subject
- end
- end
-
context 'when build is failed' do
before do
build.update!(status: :failed)
diff --git a/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb b/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
new file mode 100644
index 00000000000..e18539cc6e3
--- /dev/null
+++ b/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobArtifacts::TrackArtifactReportWorker do
+ describe '#perform', :clean_gitlab_redis_shared_state do
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project, user: user) }
+
+ subject(:perform) { described_class.new.perform(pipeline_id) }
+
+ context 'when pipeline is found' do
+ let(:pipeline_id) { pipeline.id }
+
+ it 'executed service' do
+ expect_next_instance_of(Ci::JobArtifacts::TrackArtifactReportService) do |instance|
+ expect(instance).to receive(:execute).with(pipeline)
+ end
+
+ perform
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { pipeline_id }
+ let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:start_time) { 1.week.ago }
+ let(:end_time) { 1.week.from_now }
+
+ subject(:idempotent_perform) { perform_multiple(pipeline_id, exec_times: 2) }
+
+ it 'does not try to increment again' do
+ idempotent_perform
+
+ unique_pipeline_pass = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: test_event_name,
+ start_date: start_time,
+ end_date: end_time
+ )
+ expect(unique_pipeline_pass).to eq(1)
+ end
+ end
+ end
+
+ context 'when pipeline is not found' do
+ let(:pipeline_id) { non_existing_record_id }
+
+ it 'does not execute service' do
+ allow_next_instance_of(Ci::JobArtifacts::TrackArtifactReportService) do |instance|
+ expect(instance).not_to receive(:execute)
+ end
+
+ expect { perform }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/cleanup_container_repository_worker_spec.rb b/spec/workers/cleanup_container_repository_worker_spec.rb
index edb815f426d..817b71c8cc6 100644
--- a/spec/workers/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/cleanup_container_repository_worker_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_stat
it 'executes the destroy service' do
expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new)
- .with(repository, user, params)
+ .with(container_repository: repository, current_user: user, params: params)
.and_return(service)
expect(service).to receive(:execute)
diff --git a/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb b/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb
index b9219586a0b..c24ca71eb35 100644
--- a/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb
+++ b/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Clusters::Cleanup::ProjectNamespaceWorker do
end
context 'when exceeded the execution limit' do
- subject { worker_instance.perform(cluster.id, worker_instance.send(:execution_limit))}
+ subject { worker_instance.perform(cluster.id, worker_instance.send(:execution_limit)) }
it 'logs the error' do
expect(logger).to receive(:error)
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 707fa0c9c78..5fde54b98f0 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -289,7 +289,6 @@ RSpec.describe ApplicationWorker do
perform_action
expect(worker.jobs.count).to eq args.count
- expect(worker.jobs).to all(include('enqueued_at'))
end
end
@@ -302,7 +301,6 @@ RSpec.describe ApplicationWorker do
perform_action
expect(worker.jobs.count).to eq args.count
- expect(worker.jobs).to all(include('enqueued_at'))
end
end
diff --git a/spec/workers/concerns/cluster_agent_queue_spec.rb b/spec/workers/concerns/cluster_agent_queue_spec.rb
index b5189cbd8c8..4f67102a0be 100644
--- a/spec/workers/concerns/cluster_agent_queue_spec.rb
+++ b/spec/workers/concerns/cluster_agent_queue_spec.rb
@@ -14,6 +14,5 @@ RSpec.describe ClusterAgentQueue do
end
end
- it { expect(worker.queue).to eq('cluster_agent:example') }
it { expect(worker.get_feature_category).to eq(:kubernetes_management) }
end
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
deleted file mode 100644
index c03ca9cea48..00000000000
--- a/spec/workers/concerns/cluster_queue_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ClusterQueue do
- let(:worker) do
- Class.new do
- def self.name
- 'DummyWorker'
- end
-
- include ApplicationWorker
- include ClusterQueue
- end
- end
-
- it 'sets a default pipelines queue automatically' do
- expect(worker.sidekiq_options['queue'])
- .to eq 'gcp_cluster:dummy'
- end
-end
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
index 0244535051f..7dd016fc78a 100644
--- a/spec/workers/concerns/cronjob_queue_spec.rb
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -40,10 +40,6 @@ RSpec.describe CronjobQueue do
stub_const("AnotherWorker", another_worker)
end
- it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob:dummy')
- end
-
it 'disables retrying of failed jobs' do
expect(worker.sidekiq_options['retry']).to eq(false)
end
diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
deleted file mode 100644
index beca221b593..00000000000
--- a/spec/workers/concerns/gitlab/github_import/queue_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::GithubImport::Queue do
- it 'sets the Sidekiq options for the worker' do
- worker = Class.new do
- def self.name
- 'DummyWorker'
- end
-
- include ApplicationWorker
- include Gitlab::GithubImport::Queue
- end
-
- expect(worker.sidekiq_options['queue']).to eq('github_importer:dummy')
- end
-end
diff --git a/spec/workers/concerns/pipeline_background_queue_spec.rb b/spec/workers/concerns/pipeline_background_queue_spec.rb
deleted file mode 100644
index 77c7e7440c5..00000000000
--- a/spec/workers/concerns/pipeline_background_queue_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PipelineBackgroundQueue do
- let(:worker) do
- Class.new do
- def self.name
- 'DummyWorker'
- end
-
- include ApplicationWorker
- include PipelineBackgroundQueue
- end
- end
-
- it 'sets a default object storage queue automatically' do
- expect(worker.sidekiq_options['queue'])
- .to eq 'pipeline_background:dummy'
- end
-end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
deleted file mode 100644
index 6c1ac2052e4..00000000000
--- a/spec/workers/concerns/pipeline_queue_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PipelineQueue do
- let(:worker) do
- Class.new do
- def self.name
- 'DummyWorker'
- end
-
- include ApplicationWorker
- include PipelineQueue
- end
- end
-
- it 'sets a default pipelines queue automatically' do
- expect(worker.sidekiq_options['queue'])
- .to eq 'pipeline_default:dummy'
- end
-end
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
index ae377c09b37..08ac73aac7b 100644
--- a/spec/workers/concerns/repository_check_queue_spec.rb
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -14,10 +14,6 @@ RSpec.describe RepositoryCheckQueue do
end
end
- it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check:dummy')
- end
-
it 'disables retrying of failed jobs' do
expect(worker.sidekiq_options['retry']).to eq(false)
end
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
index bf156c3b8cb..2df5b60deaf 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -49,8 +49,7 @@ RSpec.describe WaitableWorker do
expect(Gitlab::AppJsonLogger).to(
receive(:info).with(a_hash_including('message' => 'running inline',
'class' => 'Gitlab::Foo::Bar::DummyWorker',
- 'job_status' => 'running',
- 'queue' => 'foo_bar_dummy'))
+ 'job_status' => 'running'))
.once)
worker.bulk_perform_and_wait(args_list)
diff --git a/spec/workers/disallow_two_factor_for_group_worker_spec.rb b/spec/workers/disallow_two_factor_for_group_worker_spec.rb
index f30b12dd7f4..3a875727cce 100644
--- a/spec/workers/disallow_two_factor_for_group_worker_spec.rb
+++ b/spec/workers/disallow_two_factor_for_group_worker_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe DisallowTwoFactorForGroupWorker do
expect(group.reload.require_two_factor_authentication).to eq(false)
end
- it "updates group members" do
+ it "updates group members", :sidekiq_inline do
group.add_member(user, GroupMember::DEVELOPER)
described_class.new.perform(group.id)
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 3e313610054..7d11957e2df 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe EmailsOnPushWorker, :mailer do
context "when push is a force push to delete commits" do
before do
data_force_push = data.stringify_keys.merge(
- "after" => data[:before],
+ "after" => data[:before],
"before" => data[:after]
)
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 4a1bf7dbbf9..6b67c2b474c 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -257,11 +257,13 @@ RSpec.describe 'Every Sidekiq worker' do
'GeoRepositoryDestroyWorker' => 3,
'GitGarbageCollectWorker' => false,
'Gitlab::GithubImport::AdvanceStageWorker' => 3,
+ 'Gitlab::GithubImport::ImportReleaseAttachmentsWorker' => 5,
'Gitlab::GithubImport::ImportDiffNoteWorker' => 5,
'Gitlab::GithubImport::ImportIssueWorker' => 5,
'Gitlab::GithubImport::ImportIssueEventWorker' => 5,
'Gitlab::GithubImport::ImportLfsObjectWorker' => 5,
'Gitlab::GithubImport::ImportNoteWorker' => 5,
+ 'Gitlab::GithubImport::ImportProtectedBranchWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestMergedByWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestReviewWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestWorker' => 5,
@@ -271,6 +273,8 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker' => 5,
'Gitlab::GithubImport::Stage::ImportIssueEventsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportLfsObjectsWorker' => 5,
+ 'Gitlab::GithubImport::Stage::ImportAttachmentsWorker' => 5,
+ 'Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker' => 5,
'Gitlab::GithubImport::Stage::ImportNotesWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker' => 5,
@@ -311,6 +315,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Integrations::IrkerWorker' => 3,
'InvalidGpgSignatureUpdateWorker' => 3,
'IssuableExportCsvWorker' => 3,
+ 'Issues::CloseWorker' => 3,
'Issues::PlacementWorker' => 3,
'Issues::RebalancingWorker' => 3,
'IterationsUpdateStatusWorker' => 3,
@@ -356,6 +361,7 @@ RSpec.describe 'Every Sidekiq worker' do
'ObjectPool::ScheduleJoinWorker' => 3,
'ObjectStorage::BackgroundMoveWorker' => 5,
'ObjectStorage::MigrateUploadsWorker' => 3,
+ 'Onboarding::CreateLearnGitlabWorker' => 3,
'Packages::CleanupPackageFileWorker' => 0,
'Packages::Cleanup::ExecutePolicyWorker' => 0,
'Packages::Composer::CacheUpdateWorker' => false,
diff --git a/spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb b/spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb
new file mode 100644
index 00000000000..4a3ef2bf560
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::ImportProtectedBranchWorker do
+ let(:worker) { described_class.new }
+
+ let(:import_state) { build_stubbed(:import_state, :started) }
+ let(:project) { instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state) }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:importer) { instance_double('Gitlab::GithubImport::Importer::ProtectedBranchImporter') }
+
+ describe '#import' do
+ let(:json_hash) do
+ {
+ id: 'main',
+ allow_force_pushes: true
+ }
+ end
+
+ it 'imports protected branch rule' do
+ expect(Gitlab::GithubImport::Importer::ProtectedBranchImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::ProtectedBranch),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment)
+ .with(project, :protected_branch, :imported)
+
+ worker.import(project, client, json_hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb b/spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb
new file mode 100644
index 00000000000..cd53c6ee9c0
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::ImportReleaseAttachmentsWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#import' do
+ let(:import_state) { create(:import_state, :started) }
+
+ let(:project) do
+ instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state)
+ end
+
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:importer) { instance_double('Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter') }
+
+ let(:release_hash) do
+ {
+ 'release_db_id' => rand(100),
+ 'description' => <<-TEXT
+ Some text...
+
+ ![special-image](https://user-images.githubusercontent.com...)
+ TEXT
+ }
+ end
+
+ it 'imports an issue event' do
+ expect(Gitlab::GithubImport::Importer::ReleaseAttachmentsImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::ReleaseAttachments),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment)
+ .and_call_original
+
+ worker.import(project, client, release_hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
new file mode 100644
index 00000000000..c2c5e1dbf4e
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker do
+ subject(:worker) { described_class.new }
+
+ let(:project) { create(:project) }
+ let!(:group) { create(:group, projects: [project]) }
+ let(:feature_flag_state) { [group] }
+
+ describe '#import' do
+ let(:importer) { instance_double('Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter') }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+
+ before do
+ stub_feature_flags(github_importer_attachments_import: feature_flag_state)
+ end
+
+ it 'imports release attachments' do
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer).to receive(:execute).and_return(waiter)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :protected_branches)
+
+ worker.import(client, project)
+ end
+
+ context 'when feature flag is disabled' do
+ let(:feature_flag_state) { false }
+
+ it 'skips release attachments import and calls next stage' do
+ expect(Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter).not_to receive(:new)
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async).with(project.id, {}, :protected_branches)
+
+ worker.import(client, project)
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
index f9f21e4dfa2..adf20d24a7e 100644
--- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker do
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :lfs_objects)
+ .with(project.id, { '123' => 2 }, :attachments)
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
new file mode 100644
index 00000000000..0770af524a1
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:import_state) { create(:import_state, project: project) }
+
+ let(:worker) { described_class.new }
+ let(:importer) { instance_double('Gitlab::GithubImport::Importer::ProtectedBranchImporter') }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+
+ describe '#import' do
+ it 'imports all the pull requests' do
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::ProtectedBranchesImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+
+ expect(import_state)
+ .to receive(:refresh_jid_expiration)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :lfs_objects)
+
+ worker.import(client, project)
+ end
+
+ context 'when an error raised' do
+ let(:exception) { StandardError.new('_some_error_') }
+
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::Importer::ProtectedBranchesImporter) do |importer|
+ allow(importer).to receive(:execute).and_raise(exception)
+ end
+ end
+
+ it 'raises an error' do
+ expect(Gitlab::Import::ImportFailureService).to receive(:track)
+ .with(
+ project_id: project.id,
+ exception: exception,
+ error_source: described_class.name,
+ metrics: true
+ ).and_call_original
+
+ expect { worker.import(client, project) }.to raise_error(StandardError)
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab_service_ping_worker_spec.rb b/spec/workers/gitlab_service_ping_worker_spec.rb
index c88708dc50a..f17847a7b33 100644
--- a/spec/workers/gitlab_service_ping_worker_spec.rb
+++ b/spec/workers/gitlab_service_ping_worker_spec.rb
@@ -14,21 +14,36 @@ RSpec.describe GitlabServicePingWorker, :clean_gitlab_redis_shared_state do
allow(subject).to receive(:sleep)
end
- it 'does not run for GitLab.com' do
+ it 'does not run for GitLab.com when triggered from cron' do
allow(Gitlab).to receive(:com?).and_return(true)
expect(ServicePing::SubmitService).not_to receive(:new)
subject.perform
end
+ it 'runs for GitLab.com when triggered manually' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ expect(ServicePing::SubmitService).to receive(:new)
+
+ subject.perform('triggered_from_cron' => false)
+ end
+
it 'delegates to ServicePing::SubmitService' do
- expect_next_instance_of(ServicePing::SubmitService, payload: payload) do |service|
+ expect_next_instance_of(ServicePing::SubmitService, payload: payload, skip_db_write: false) do |service|
expect(service).to receive(:execute)
end
subject.perform
end
+ it 'passes Hash arguments to ServicePing::SubmitService' do
+ expect_next_instance_of(ServicePing::SubmitService, payload: payload, skip_db_write: true) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject.perform('skip_db_write' => true)
+ end
+
context 'payload computation' do
it 'creates RawUsageData entry when there is NO entry with the same recorded_at timestamp' do
expect { subject.perform }.to change { RawUsageData.count }.by(1)
@@ -46,7 +61,7 @@ RSpec.describe GitlabServicePingWorker, :clean_gitlab_redis_shared_state do
allow(::ServicePing::BuildPayload).to receive(:new).and_raise(error)
expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error)
- expect_next_instance_of(::ServicePing::SubmitService, payload: nil) do |service|
+ expect_next_instance_of(::ServicePing::SubmitService, payload: nil, skip_db_write: false) do |service|
expect(service).to receive(:execute)
end
diff --git a/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb b/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
new file mode 100644
index 00000000000..c0b32515d15
--- /dev/null
+++ b/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::FetchGoogleIpListWorker do
+ describe '#perform' do
+ it 'returns success' do
+ allow_next_instance_of(GoogleCloud::FetchGoogleIpListService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :success })
+ end
+
+ expect(described_class.new.perform).to eq({ status: :success })
+ end
+ end
+end
diff --git a/spec/workers/groups/update_two_factor_requirement_for_members_worker_spec.rb b/spec/workers/groups/update_two_factor_requirement_for_members_worker_spec.rb
new file mode 100644
index 00000000000..9d202b9452f
--- /dev/null
+++ b/spec/workers/groups/update_two_factor_requirement_for_members_worker_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::UpdateTwoFactorRequirementForMembersWorker do
+ let_it_be(:group) { create(:group) }
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'calls #update_two_factor_requirement_for_members' do
+ allow(Group).to receive(:find_by_id).with(group.id).and_return(group)
+ expect(group).to receive(:update_two_factor_requirement_for_members)
+
+ worker.perform(group.id)
+ end
+
+ context 'when group not found' do
+ it 'returns nil' do
+ expect(worker.perform(non_existing_record_id)).to be_nil
+ end
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:subject) { described_class.new.perform(group.id) }
+
+ it 'requires 2fa for group members correctly' do
+ group.update!(require_two_factor_authentication: true)
+ user = create(:user, require_two_factor_authentication_from_group: false)
+ group.add_member(user, GroupMember::OWNER)
+
+ # Using subject inside this block will process the job multiple times
+ subject
+
+ expect(user.reload.require_two_factor_authentication_from_group).to be true
+ end
+ end
+ end
+end
diff --git a/spec/workers/issues/close_worker_spec.rb b/spec/workers/issues/close_worker_spec.rb
new file mode 100644
index 00000000000..41611447db1
--- /dev/null
+++ b/spec/workers/issues/close_worker_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Issues::CloseWorker do
+ describe "#perform" do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:issue) { create(:issue, project: project, author: user) }
+
+ let(:commit) { project.commit }
+ let(:opts) do
+ { "closed_by" => user&.id, "commit_hash" => commit.to_hash }
+ end
+
+ subject(:worker) { described_class.new }
+
+ describe "#perform" do
+ context "when the user can update the issues" do
+ it "closes the issues" do
+ worker.perform(project.id, issue.id, issue.class.to_s, opts)
+
+ issue.reload
+
+ expect(issue.closed?).to eq(true)
+ end
+
+ it "closes external issues" do
+ external_issue = ExternalIssue.new("foo", project)
+ closer = instance_double(Issues::CloseService, execute: true)
+
+ expect(Issues::CloseService).to receive(:new).with(project: project, current_user: user).and_return(closer)
+ expect(closer).to receive(:execute).with(external_issue, commit: commit)
+
+ worker.perform(project.id, external_issue.id, external_issue.class.to_s, opts)
+ end
+ end
+
+ context "when the user can not update the issues" do
+ it "does not close the issues" do
+ other_user = create(:user)
+ opts = { "closed_by" => other_user.id, "commit_hash" => commit.to_hash }
+
+ worker.perform(project.id, issue.id, issue.class.to_s, opts)
+
+ issue.reload
+
+ expect(issue.closed?).to eq(false)
+ end
+ end
+ end
+
+ shared_examples "when object does not exist" do
+ it "does not call the close issue service" do
+ expect(Issues::CloseService).not_to receive(:new)
+
+ expect { worker.perform(project.id, issue.id, issue.class.to_s, opts) }
+ .not_to raise_exception
+ end
+ end
+
+ context "when the project does not exist" do
+ before do
+ allow(Project).to receive(:find_by_id).with(project.id).and_return(nil)
+ end
+
+ it_behaves_like "when object does not exist"
+ end
+
+ context "when the user does not exist" do
+ before do
+ allow(User).to receive(:find_by_id).with(user.id).and_return(nil)
+ end
+
+ it_behaves_like "when object does not exist"
+ end
+
+ context "when the issue does not exist" do
+ before do
+ allow(Issue).to receive(:find_by_id).with(issue.id).and_return(nil)
+ end
+
+ it_behaves_like "when object does not exist"
+ end
+ end
+end
diff --git a/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb b/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb
index 53116815ce7..0a896d864b7 100644
--- a/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb
+++ b/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb
@@ -19,11 +19,11 @@ RSpec.describe Namespaces::OnboardingIssueCreatedWorker, '#perform' do
let(:job_args) { [namespace.id] }
it 'sets the onboarding progress action' do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
subject
- expect(OnboardingProgress.completed?(namespace, :issue_created)).to eq(true)
+ expect(Onboarding::Progress.completed?(namespace, :issue_created)).to eq(true)
end
end
end
diff --git a/spec/workers/namespaces/process_sync_events_worker_spec.rb b/spec/workers/namespaces/process_sync_events_worker_spec.rb
index c15a74a2934..5e5179eab62 100644
--- a/spec/workers/namespaces/process_sync_events_worker_spec.rb
+++ b/spec/workers/namespaces/process_sync_events_worker_spec.rb
@@ -11,6 +11,30 @@ RSpec.describe Namespaces::ProcessSyncEventsWorker do
include_examples 'an idempotent worker'
+ describe 'deduplication' do
+ before do
+ stub_const("Ci::ProcessSyncEventsService::BATCH_SIZE", 2)
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
+ it 'has an option to reschedule once if deduplicated' do
+ expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once })
+ end
+
+ it 'expect the job to enqueue itself again if there was more items to be processed', :sidekiq_inline do
+ Namespaces::SyncEvent.delete_all # delete the sync_events that have been created by triggers of previous groups
+ create_list(:sync_event, 3, namespace_id: group1.id)
+ # It's called more than twice, because the job deduplication and rescheduling calls the perform_async again
+ expect(described_class).to receive(:perform_async).at_least(:twice).and_call_original
+ expect do
+ described_class.perform_async
+ end.to change(Namespaces::SyncEvent, :count).from(3).to(0)
+ end
+ end
+
describe '#perform' do
subject(:perform) { worker.perform }
diff --git a/spec/workers/packages/helm/extraction_worker_spec.rb b/spec/workers/packages/helm/extraction_worker_spec.rb
index daebbda3077..70a090d6989 100644
--- a/spec/workers/packages/helm/extraction_worker_spec.rb
+++ b/spec/workers/packages/helm/extraction_worker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Packages::Helm::ExtractionWorker, type: :worker do
describe '#perform' do
- let_it_be(:package) { create(:helm_package, without_package_files: true, status: 'processing')}
+ let_it_be(:package) { create(:helm_package, without_package_files: true, status: 'processing') }
let!(:package_file) { create(:helm_package_file, without_loaded_metadatum: true, package: package) }
let(:package_file_id) { package_file.id }
diff --git a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
index 563bbdef1be..70ffef5342e 100644
--- a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
@@ -25,8 +25,8 @@ RSpec.describe PagesDomainSslRenewalCronWorker do
end
let!(:domain_with_failed_auto_ssl) do
- create(:pages_domain, :without_certificate, :without_key, project: project,
- auto_ssl_enabled: true, auto_ssl_failed: true)
+ create(:pages_domain, :without_certificate, :without_key,
+ project: project, auto_ssl_enabled: true, auto_ssl_failed: true)
end
let!(:domain_with_expired_auto_ssl) do
diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb
index 5ddfd5b43b9..ad714d8d11e 100644
--- a/spec/workers/pages_worker_spec.rb
+++ b/spec/workers/pages_worker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe PagesWorker, :sidekiq_inline do
let(:project) { create(:project) }
- let(:ci_build) { create(:ci_build, project: project)}
+ let(:ci_build) { create(:ci_build, project: project) }
it 'calls UpdatePagesService' do
expect_next_instance_of(Projects::UpdatePagesService, project, ci_build) do |service|
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 3df26c774ba..a445db3a276 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -115,25 +115,37 @@ RSpec.describe ProcessCommitWorker do
end
describe '#close_issues' do
- context 'when the user can update the issues' do
- it 'closes the issues' do
+ it 'creates Issue::CloseWorker jobs' do
+ expect do
worker.close_issues(project, user, user, commit, [issue])
+ end.to change(Issues::CloseWorker.jobs, :size).by(1)
+ end
+
+ context 'when process_issue_closure_in_background flag is disabled' do
+ before do
+ stub_feature_flags(process_issue_closure_in_background: false)
+ end
- issue.reload
+ context 'when the user can update the issues' do
+ it 'closes the issues' do
+ worker.close_issues(project, user, user, commit, [issue])
- expect(issue.closed?).to eq(true)
+ issue.reload
+
+ expect(issue.closed?).to eq(true)
+ end
end
- end
- context 'when the user can not update the issues' do
- it 'does not close the issues' do
- other_user = create(:user)
+ context 'when the user can not update the issues' do
+ it 'does not close the issues' do
+ other_user = create(:user)
- worker.close_issues(project, other_user, other_user, commit, [issue])
+ worker.close_issues(project, other_user, other_user, commit, [issue])
- issue.reload
+ issue.reload
- expect(issue.closed?).to eq(false)
+ expect(issue.closed?).to eq(false)
+ end
end
end
end
@@ -189,20 +201,4 @@ RSpec.describe ProcessCommitWorker do
end
end
end
-
- describe '#build_commit' do
- it 'returns a Commit' do
- commit = worker.build_commit(project, id: '123')
-
- expect(commit).to be_an_instance_of(Commit)
- end
-
- it 'parses date strings into Time instances' do
- commit = worker.build_commit(project,
- id: '123',
- authored_date: Time.current.to_s)
-
- expect(commit.authored_date).to be_a_kind_of(Time)
- end
- end
end
diff --git a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
index ec10c66968d..50b5b0a6e7b 100644
--- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
+++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
@@ -85,86 +85,58 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker do
end
end
- context 'when delete inactive projects feature is enabled' do
+ context 'when delete inactive projects feature is enabled', :clean_gitlab_redis_shared_state, :sidekiq_inline do
before do
stub_application_setting(delete_inactive_projects: true)
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(inactive_projects_deletion: false)
- end
-
- it 'does not invoke Projects::InactiveProjectsDeletionNotificationWorker' do
- expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async)
- expect(::Projects::DestroyService).not_to receive(:new)
-
- worker.perform
- end
-
- it 'does not delete the inactive projects' do
- worker.perform
-
- expect(inactive_large_project.reload.pending_delete).to eq(false)
+ it 'invokes Projects::InactiveProjectsDeletionNotificationWorker for inactive projects' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:hset).with('inactive_projects_deletion_warning_email_notified',
+ "project:#{inactive_large_project.id}", Date.current)
end
+ expect(::Projects::InactiveProjectsDeletionNotificationWorker).to receive(:perform_async).with(
+ inactive_large_project.id, deletion_date).and_call_original
+ expect(::Projects::DestroyService).not_to receive(:new)
- it_behaves_like 'worker is running for more than 4 minutes'
- it_behaves_like 'worker finishes processing in less than 4 minutes'
+ worker.perform
end
- context 'when feature flag is enabled', :clean_gitlab_redis_shared_state, :sidekiq_inline do
- before do
- stub_feature_flags(inactive_projects_deletion: true)
- end
-
- it 'invokes Projects::InactiveProjectsDeletionNotificationWorker for inactive projects' do
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis).to receive(:hset).with('inactive_projects_deletion_warning_email_notified',
- "project:#{inactive_large_project.id}", Date.current)
- end
- expect(::Projects::InactiveProjectsDeletionNotificationWorker).to receive(:perform_async).with(
- inactive_large_project.id, deletion_date).and_call_original
- expect(::Projects::DestroyService).not_to receive(:new)
-
- worker.perform
+ it 'does not invoke InactiveProjectsDeletionNotificationWorker for already notified inactive projects' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}",
+ Date.current.to_s)
end
- it 'does not invoke InactiveProjectsDeletionNotificationWorker for already notified inactive projects' do
- Gitlab::Redis::SharedState.with do |redis|
- redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}",
- Date.current.to_s)
- end
+ expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async)
+ expect(::Projects::DestroyService).not_to receive(:new)
- expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async)
- expect(::Projects::DestroyService).not_to receive(:new)
+ worker.perform
+ end
- worker.perform
+ it 'invokes Projects::DestroyService for projects that are inactive even after being notified' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}",
+ 15.months.ago.to_date.to_s)
end
- it 'invokes Projects::DestroyService for projects that are inactive even after being notified' do
- Gitlab::Redis::SharedState.with do |redis|
- redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}",
- 15.months.ago.to_date.to_s)
- end
-
- expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async)
- expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_user, {})
- .at_least(:once).and_call_original
+ expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async)
+ expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_user, {})
+ .at_least(:once).and_call_original
- worker.perform
+ worker.perform
- expect(inactive_large_project.reload.pending_delete).to eq(true)
+ expect(inactive_large_project.reload.pending_delete).to eq(true)
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.hget('inactive_projects_deletion_warning_email_notified',
- "project:#{inactive_large_project.id}")).to be_nil
- end
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.hget('inactive_projects_deletion_warning_email_notified',
+ "project:#{inactive_large_project.id}")).to be_nil
end
-
- it_behaves_like 'worker is running for more than 4 minutes'
- it_behaves_like 'worker finishes processing in less than 4 minutes'
end
+ it_behaves_like 'worker is running for more than 4 minutes'
+ it_behaves_like 'worker finishes processing in less than 4 minutes'
+
it_behaves_like 'an idempotent worker'
end
end
diff --git a/spec/workers/projects/process_sync_events_worker_spec.rb b/spec/workers/projects/process_sync_events_worker_spec.rb
index 963e0ad1028..202942ce905 100644
--- a/spec/workers/projects/process_sync_events_worker_spec.rb
+++ b/spec/workers/projects/process_sync_events_worker_spec.rb
@@ -10,6 +10,14 @@ RSpec.describe Projects::ProcessSyncEventsWorker do
include_examples 'an idempotent worker'
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
+ it 'has an option to reschedule once if deduplicated' do
+ expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once })
+ end
+
describe '#perform' do
subject(:perform) { worker.perform }
diff --git a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
index 3de59670f8d..84315fd6ee9 100644
--- a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
+++ b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe PurgeDependencyProxyCacheWorker do
let_it_be(:user) { create(:admin) }
- let_it_be_with_refind(:blob) { create(:dependency_proxy_blob )}
+ let_it_be_with_refind(:blob) { create(:dependency_proxy_blob ) }
let_it_be_with_reload(:group) { blob.group }
- let_it_be_with_refind(:manifest) { create(:dependency_proxy_manifest, group: group )}
+ let_it_be_with_refind(:manifest) { create(:dependency_proxy_manifest, group: group ) }
let_it_be(:group_id) { group.id }
subject { described_class.new.perform(user.id, group_id) }
diff --git a/spec/workers/releases/manage_evidence_worker_spec.rb b/spec/workers/releases/manage_evidence_worker_spec.rb
index 2fbfb6c9dc1..886fcd346eb 100644
--- a/spec/workers/releases/manage_evidence_worker_spec.rb
+++ b/spec/workers/releases/manage_evidence_worker_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Releases::ManageEvidenceWorker do
context 'when evidence has already been created' do
let(:release) { create(:release, project: project, released_at: 1.hour.since) }
- let!(:evidence) { create(:evidence, release: release )}
+ let!(:evidence) { create(:evidence, release: release ) }
it_behaves_like 'does not create a new Evidence record'
end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
index 8d7d488094f..44b8fa21be4 100644
--- a/spec/workers/remove_expired_members_worker_spec.rb
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -56,10 +56,27 @@ RSpec.describe RemoveExpiredMembersWorker do
expect(Member.find_by(user_id: expired_project_bot.id)).to be_nil
end
- it 'deletes expired project bot' do
- worker.perform
+ context 'when user_destroy_with_limited_execution_time_worker is enabled' do
+ it 'initiates project bot removal' do
+ worker.perform
+
+ expect(
+ Users::GhostUserMigration.where(user: expired_project_bot,
+ initiator_user: nil)
+ ).to be_exists
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'deletes expired project bot' do
+ worker.perform
- expect(User.exists?(expired_project_bot.id)).to be(false)
+ expect(User.exists?(expired_project_bot.id)).to be(false)
+ end
end
end
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
index 829abc7d895..146228c0852 100644
--- a/spec/workers/repository_check/dispatch_worker_spec.rb
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe RepositoryCheck::DispatchWorker do
end
it 'dispatches work to RepositoryCheck::BatchWorker' do
+ expect_next_instance_of(Gitlab::GitalyClient::ServerService) do |service|
+ expect(service).to receive(:readiness_check).and_return({ success: true })
+ end
+
expect(RepositoryCheck::BatchWorker).to receive(:perform_async).at_least(:once)
subject.perform
diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
index 26d9460d73e..f93d02e86c0 100644
--- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do
it 'uses a cronjob queue' do
expect(worker.sidekiq_options_hash).to include(
- 'queue' => 'cronjob:ssh_keys_expired_notification',
'queue_namespace' => :cronjob
)
end
diff --git a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
index e907d035020..ed6701532a5 100644
--- a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do
it 'uses a cronjob queue' do
expect(worker.sidekiq_options_hash).to include(
- 'queue' => 'cronjob:ssh_keys_expiring_soon_notification',
'queue_namespace' => :cronjob
)
end
diff --git a/spec/workers/users/deactivate_dormant_users_worker_spec.rb b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
index 263ca31e0a0..a8318de669b 100644
--- a/spec/workers/users/deactivate_dormant_users_worker_spec.rb
+++ b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
using RSpec::Parameterized::TableSyntax
describe '#perform' do
- let_it_be(:dormant) { create(:user, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date) }
+ let_it_be(:dormant) { create(:user, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date) }
let_it_be(:inactive) { create(:user, last_activity_on: nil, created_at: User::MINIMUM_DAYS_CREATED.days.ago.to_date) }
let_it_be(:inactive_recently_created) { create(:user, last_activity_on: nil, created_at: (User::MINIMUM_DAYS_CREATED - 1).days.ago.to_date) }
@@ -14,7 +14,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
it 'does not run for GitLab.com' do
expect(Gitlab).to receive(:com?).and_return(true)
- expect(Gitlab::CurrentSettings).not_to receive(:current_application_settings)
+ # Now makes a call to current settings to determine period of dormancy
worker.perform
@@ -48,7 +48,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
end
with_them do
it 'deactivates certain user types' do
- user = create(:user, user_type: user_type, state: :active, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
+ user = create(:user, user_type: user_type, state: :active, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
worker.perform
@@ -57,8 +57,8 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
end
it 'does not deactivate non-active users' do
- human_user = create(:user, user_type: :human, state: :blocked, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
- service_user = create(:user, user_type: :service_user, state: :blocked, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
+ human_user = create(:user, user_type: :human, state: :blocked, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
+ service_user = create(:user, user_type: :service_user, state: :blocked, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
worker.perform
diff --git a/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb b/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb
new file mode 100644
index 00000000000..f42033fdb9c
--- /dev/null
+++ b/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::MigrateRecordsToGhostUserInBatchesWorker do
+ include ExclusiveLeaseHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform', :clean_gitlab_redis_shared_state do
+ it 'executes service with lease' do
+ lease_key = described_class.name.underscore
+
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_next_instance_of(Users::MigrateRecordsToGhostUserInBatchesService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
+ worker.perform
+ end
+ end
+
+ include_examples 'an idempotent worker' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: create(:group)) }
+ let_it_be(:issue) { create(:issue, project: project, author: user, last_edited_by: user) }
+
+ subject { worker.perform }
+
+ before do
+ create(:ghost_user_migration, user: user, initiator_user: user)
+ end
+
+ it 'migrates issue to ghost user' do
+ subject
+
+ expect(issue.reload.author).to eq(User.ghost)
+ expect(issue.last_edited_by).to eq(User.ghost)
+ end
+ end
+
+ context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ before do
+ stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ end
+
+ it 'does not execute the service' do
+ expect(Users::MigrateRecordsToGhostUserInBatchesService).not_to receive(:new)
+
+ worker.perform
+ end
+ end
+end